본문으로 건너뛰기

스레드와 병행성

개요

스레드는 CPU 이용의 기본 단위이다. 스레드는 스레드 ID, 프로그램 카운터(PC), 레지스터 집합, 그리고 스택으로 구성된다. 스레드는 같은 프로세스에 속한 다른 스레드와 코드, 데이터 섹션, 그리고 열린 파일이나 신호와 같은 운영체제 자원들을 공유한다. 전통적인 프로세스는 하나의 제어 스레드를 가지고 있다. 만일 프로세스가 다수의 제어 스레드를 가진다면, 프로세스는 동시에 하나 이상의 작업을 수행할 수 있다.

장점

다중 스레드 프로그래밍의 이점은 다음과 같다.

  1. 응답성(responsiveness): 대화형 응용프로그램을 다중 스레드화하면 응용 프로그램의 일부분이 봉쇄되거나, 응용 프로그램이 긴 작업을 수행하더라도 프로글매의 수행이 계속되는 것을 허용함으로써, 사용자에 대한 응답성을 증가시킨다. 이 특징은 사용자 인터페이스를 설계하는 데 있어 매우 유용하다. 예를 들어 사용자가 시간이 많이 걸리는 연산을 시작했을 때 단일 스레드는 그 연산이 완료될 때까지 사용자에게 응답하지 않을 것이다. 대조적으로 시간이 오래 걸리는 연산이 별도의 비동기적 스레드에서 실행된다면 응용은 여전히 사용자에게 응답할 수 있다.
  2. 자원 공유(resource sharing): 프로세스는 공유 메모리와 메시지 전달 기법을 통하여 자원을 공유할 수 있다. 이러한 기법은 프로그래머에 의해 명시적으로 처리되어야 한다. 그러나 스레드는 자동으로 그들이 속한 프로세스의 자원들과 메모리를 공유한다. 코드와 데이터 공유의 이점은 한 응용 프로그램이 같은 주소 공간 내에 여러 개의 다른 작업을 하는 스레드를 가질 수 있다는 점이다.
  3. 경제성(economy): 프로세스를 생성을 위해 메모리와 자원을 할당하는 것은 비용이 많이 든다. 스레드는 자신이 속한 프로세스의 자원을 공유하기 때문에, 스레드를 생성하고 문맥교환하는 것이 더 경제적이다. 오버헤드의 차이를 경험적으로 측정하는 것은 어려울 수 있지만 일반적으로 스레드 생성은 프로세스 생성보다 시간과 메모리를 덜 소비한다. 또한 문맥 교환은 일반적으로 프로세스 사이보다 스레드 사이에서 더 빠르다.
  4. 규모 적응성(scalability): 다중 스레드의 이점은 다중 처리기 구조에서 더욱 증가할 수 있다. 다중 처리기 구조에서는 각각의 스레드가 다른 처리기에서 병렬로 수행될 수 있기 때문이다. 단일 스레드 프로세스는 처리기가 아무리 많더라도 오직 한 처리기에서만 실행된다.

다중 코어 프로그래밍

병행 시스템은 모든 작업이 진행되게 하여 둘 이상의 작업을 지원한다. 이에 반해 병렬 시스템은 둘 이상의 작업을 동시에 수행할 수 있다. 따라서, 병렬성 없이 병행성을 가질 수 있다. 다주 처리기 및 다중 코어 아키텍처가 출현하기 전에 대부분의 컴퓨터 시스템에는 단일 프로세서만 있었으며 CPU 스케줄러는 프로세스 간에 빠르게 전환해 각 프로세스가 진행되도록 하여 병렬성의 환상을 제공하였다. 이러한 프로세스는 병행하게 실행되었지만 병렬로 실행되지는 않았다.

다중 스레드 모델

사용자 스레드를 위해서는 사용자 수준에서, 또는 커널 스레드를 위해서는 커널 수준에서 제공된다. 사용자 스레드는 커널 위에서 지원되며 커널의 지원 없이 관리된다. 반면에 커널 스레드는 운영체제에 의해 직접 지원되고 관리된다.

다대일 모델

다대일 모델은 많은 사용자 수준 스레드를 하나의 커널 스레드로 사상한다. 스레드 관리는 사용자 공간의 스레드 라이브러리에 의해 행해진다. 한번에 하나의 스레드만이 커널에 접근할 수 있기 때문에 다중 스레드가 다중 코어 시스템에서 병렬로 실행될 수 없다.

일대일 모델

일대일 몯레은 각 사용자 스레드를 각각 하나의 커널 스레드로 사상한다. 이 모델은 하나의 스레드가 봉쇄적인 시스템 콜을 호출하더라도 다른 스레드가 실행될 수 있기 때문에 다대일 모델보다 더 많은 병렬성을 제공한다. 단점은 사용자 스레드를 만들려면 해당 커널 스레드를 만들어야 하며 많은 수의 커널 스레드가 시스템 성능에 부담을 줄 수도 있다.

다대다 모델

다대다 모델은 여러 개의 사용자 수준 스레드를 그보다 작은 수, 혹은 같은 수의 커널 스레드로 멀티플렉스한다.

다대일 모델은 개발자가 원하는 만큼의 사용자 수준 스레드를 생성하도록 허용하지만, 커널은 한 번에 하나의 스레드만 스케줄 할 수 있기 때문에 진정한 병렬 실행을 획득할 수 없다. 일대일 모델은 더 많은 병행 실행을 제공하지만 개발자가 한 응용프로그램 내에 너무 많은 스레드를 생성하지 않도록 주의해야한다. 다대다 모델은 이러한 두 가지의 단점들을 어느 정도 해결했다. 개발자는 필요한 만큼 많은 수준 스레드를 생성할 수 있다. 그리고 상응하는 커널 스레드가 다중처리기에서 병렬로 수행될 수 있다. 또한, 스레드가 blocking 시스템 콜을 발생시켰을 때, 커널이 다른 스레드의 수행을 스케줄할 수 있다.

다대다 모델의 변형은 여전히 많은 사용자 스레드를 적거나 같은 수의 커널 스레드로 멀티 플렉스 시키지만 또한 한 사용자 스레드가 하나의 커널 스레드에만 연관되는 것을 허용한다. 이 변형은 때로 two-level model이라고 불리며 다대다 모델이 논의된 모델 중 가장 융통성 있는 것처럼 보이지만 실제로는 구현하기가 어렵다. 또한 대부분의 시스템에서 처리 코어 수가 증가함에 따라 커널 스레드 수를 제한하는 것의 중요성이 줄어들었다.

결과적으로 대부분의 운영체제는 이제 일대일 모델을 사용한다.

스레드 라이브러리

스레드 라이브러리는 프로그래머에게 스레드를 생성하고 관리하기 위한 API를 제공한다.스레드 라이브러리를 구현하는데는 주된 두가지 방법이 있다.

  1. 커널의 지원 없이 완전히 사용자 공간에서만 라이브러리를 제공하는 것이다. 라이브러리의 모든 코드와 자료구조는 사용자 공간에 존재한다. 라이브러리의 함수를 호출하는 것은 시스템 콜이 아니라 사용자 공간의 지역 함수를 호출하게 된다는 것을 의미한다.
  2. 운영체제에 의해 지원되는 커널 수준 라이브러를 구현하는 것이다. 이 경우 라이브러리를 위한 코드와 자료구조는 커널 공간에 존재한다. 라이브러리 API를 호출하는 것은 커널 시스템 콜을 부르는 결과를 낳는다.

스레드의 생성의 일반적인 전략에는 비동기 스레딩과 동기 스레딩이 있다.

비동기 스레딩은 부모가 자식 스레드를 생성한 후 부모는 자신의 실행을 재개하여 부모와 자식 스레드가 서로 독립적으로 병행하게 실행된다. 스레드가 독립적이기 때문에 스레드 사이의 데이터 공유는 거의 없다. 비동기 스레딩은 다중 스레드 서버에서 사용되는 전략이고 반응형 사용자 인터페이스를 설계하는데에도 흔히 사용된다.

동기 스레딩은 부모 스레드가 하나 이상의 자식 스레드를 생성하고 자식 스레드 모두가 종료할 때까지 기다렸다가 자신의 실행을 재개하는 방식을 말한다. 여기서 부모가 생성한 스레드는 병행하게 실행되지만 부모는 자식들의 작업이 끝날 때까지 실행을 계속할 수 없다. 부모는 자식들이 조인한 후에야 실행을 재개할 수 있다. 통상 동기 스레딩은 스레드 사이의 상당한양의 데이터 공유를 수반한다. 예를 들어 부모 스레드는 자식들이 계산한 결과를 통합할 수 있다.

암묵적 스레딩(Implicit Threading)

다중 코어 처리의 지속적 성장에 따라 수백 또는 심지어 수천 개의 스레드를 가진 응용 프로그램이 등장하게 되었다. 이러한 스레드 관리의 어려움을 도와주는 한가지 방법은 스레딩의 생성과 관리 책임을 응용 프로그램 개발자로부터 컴파일러와 실행시간 라이브러리에게 넘겨주는 것이다. 암묵적 스레딩이라고 불리는 이 전략은 점점 더 널리 사용되고 있다.

스레드 풀

새로운 스레드를 매 요청마다 만들어주는 것은, 그때마다 새로운 프로세스를 만들어주는 것보다는 확실히 더 진보된 방법임은 틀림없지만, 다중 스레드 서버는 아직도 여러 문제를 가지고 있다. 첫 번째 문제는 서비스할 때마다 스레드를 생성하는 데 소요되는 시간이다. 특히 이 스레드는 이 일만 끝나면 곧장 폐기될 것이라는 점을 염두해두면 더 그렇다. 두 번째 이슈는 모든 요청마다 새 스레드를 만들어서 서비스해 준다면 시스템에서 동시에 실행할 수 있는 최대 스레드 수가 몇 개까지 가능할 수 있는 것인지 한계를 정해야 한다. 스레드를 무한정 만들면 언젠가는 CPU 시간, 메모리 공간 같은 시스템 자원이 고갈된다. 이러한 문제들을 해결해 줄 수 있는 방법의 하나가 스레드 풀(pool)이다.

스레드 풀의 기본 아이디어는 프로세스를 시작할 때 아예 일정한 수의 스레드를 미리 풀로 만들어두는 것이다. 이 스레드들은 평소에는 하는 일 없이 일감을 기다리게 된다. 서버는 스레드를 생성하지 않고 요청을 받으면 대신 스레드 풀에 제출하고 추가요청 대기를 한다. 풀에 사용 가능한 스레드가 있으면 깨어나고 요청이 즉시 서비스된다.

Fork Join

스레드 생성 전략은 종종 fork-join 모델로 알려져 있다. 이 메소드를 사용하면 메인 부모 스레드가 하나 이상의 자식 스레드를 생성(fork)한 다음 자식의 종료를 기다린 후 join하고 그 시점부터 자식의 결과를 확인하고 결합할 수 있다.