공룡책 요약하기 04
Threads
거의 모든 현대 운영체제는 한 프로세스가 다중 스레드를 포함하는 특성을 제공한다.
스레드는 CPU 이용의 기본 단위이다. 스레드는 스레드 ID, 프로그램 카운터, 레지스터 집합, 그리고 스택으로 구성된다.
스레드는 같은 프로세스에 속한 다른 스레드와 운영체제 자원들을 공유한다.
만일 프로세스가 다수의 제어 스레드를 가진다면, 프로세스는 동시에 하나 이상의 작업을 수행할 수 있다.
스레드가 대중화되기 전에는 응용 프로그램이 여러 개의 비슷한 작업들을 수행할 필요가 있는 상황에서 서비스 요청이 들어오면 그 요청을 수행할 별도의 프로세스를 생성하는 식의 방식으로 병렬 작업을 하였다.
하지만 프로세스 생성 작업은 많은 오버헤드를 감수해야 하므로 대부분의 경우 프로세스 안에 여러 스레드를 만들어 나가는 것이 더 효율적이다.
예를 들어 웹 서버가 다중 스레드화 되면, 서버는 클라이언트의 요청을 listen하는 별도의 스레드를 생성하고, 요청이 들어올 시 요청을 서비스할 새로운 스레드를 생성하고 추가적인 요청을 listen하기 위한 작업을 재개한다.
다중 스레드 프로그래밍의 장점을 소개하겠다.
- 응답성
- 응용 프로그램의 일부가 봉쇄되거나, 또는 응용 프로그램이 긴 작업을 수행하더라도 프로그램의 수행이 계속되는 것을 허용함으로써, 사용자에 대한 응답성을 증가시킨다.
- 자원 공유
- 프로세스는 자원 공유를 명시적으로 처리하는 반면, 스레드는 자동적으로 그들이 속한 프로세스의 자원들과 메모리를 공유한다.
- 경제성
- 프로세스 생성을 위해 메모리와 자원을 할당하는 것은 비용이 많이 든다. 스레드는 자신이 속한 프로세스의 자원들을 공유하기 때문에, 스레드를 생성하고 문맥 교환하는 것이 보다 더 경제적이다.
- 규모 적응성
- 다중 스레드의 이점은 다중 처리기 구조에서 더욱 증가할 수 있다. 다중 처리기 구조에서는 각각의 스레드가 다른 처리기에서 병렬로 수행될 수 있기 때문이다. 단일 스레드 프로세스는 처리기가 아무리 많더라도 오직 한 처리기에서만 실행된다.
하나의 코어는 한 번에 오직 하나의 스레드만 실행할 수 있기 때문에 단일 코어 시스템 상에서 병행성은 단순히 스레드의 실행이 시간에 따라 교대로 실행된다는 것을 의미한다(병행 실행).
그러나 여러 코어를 가진 시스템에서는 시스템이 개별 스레드를 각 코어에 배정할 수 있기 때문에 병행성은 스레드들이 병렬적으로 실행될 수 있다는 것을 뜻한다(병렬 실행).
컴퓨터 시스템이 하나의 처리기만을 가지고 있다면 CPU 스케쥴러는 시스템의 프로세스 사이를 빠르게 오가며 모든 프레소스를 진행시켜 마치 병렬 실행하는 듯한 환상을 주도록 설계된다.
병렬 실행의 유형에는 데이터 병렬과 태스크 병렬 실행이 있다.
데이터 병렬 실행은 동일한 데이터의 부분집합을 다수의 계산 코어에 분배한 뒤 각 코어에서 동일한 연산을 실행하는 데 초점을 맞춘다.
태스크 병렬 실행은 데이터가 아니라 태스크(스레드)를 다수의 코어에 분배한다. 각 스레드는 고유의 연산을 실행한다. 다른 스레드들이 동일한 데이터에 대해 연산을 실행할 수 있고 혹은 서로 다른 데이터에 연산을 실행할 수 있다.
근본적으로 데이터 병렬 실행은 데이터가 코어에 분배되어야 하고 태스크 병렬 실행은 태스크가 분배되어야 한다. 그러나 현실적으로는 어느 한쪽의 특성만을 가지는 응용은 거의 없으며 대부분 이 두 전략을 혼용한다.
스레드의 종류는 스레드를 지원하는 주체에 따라 2가지로 나눌 수 있다.
스레드를 위한 지원은 사용자 스레드를 위해서는 사용자 수준에서, 또는 커널 스레드를 위해서는 커널 수준에서 제공된다
- 사용자 레벨 스레드 (User-Level Thread)
- 사용자 스레드는 커널 영역의 상위에서 지원되며 일반적으로 사용자 레벨의 라이브러리를 통해 구현되며, 라이브러리는 스레드의 생성 및 스케줄링 등에 관한 관리 기능을 제공한다. 동일한 메모리 영역에서 스레드가 생성 및 관리되므로 속도가 빠른 장점이 있는 반면, 여러 개의 사용자 스레드 중 하나의 스레드가 시스템 호출 등으로 중단되면 나머지 모든 스레드 역시 중단되는 단점이 있다. 이는 커널이 프로세스 내부의 스레드를 인식하지 못하며 해당 프로세스를 대기 상태로 전환시키기 때문이다.
- 커널 레벨 스레드 (Kernel-Level Thread)
- 커널 스레드는 운영체제가 지원하는 스레드 기능으로 구현되며, 커널이 스레드의 생성 및 스케줄링 등을 관리한다. 스레드가 시스템 호출 등으로 중단되더라도, 커널은 프로세스 내의 다른 스레드를 중단시키지 않고 계속 실행시켜준다. 다중처리기 환경에서 커널은 여러 개의 스레드를 각각 다른 처리기에 할당할 수 있다. 다만, 사용자 스레드에 비해 생성 및 관리하는 것이 느리다.
사용자 스레드는 커널 위에서 커널의 지원 없이 관리된다. 반면 커널 스레드는 운영체제에 의해 직접 지원되고 관리된다.
사용자와 스레드와 커널 스레드는 어떤 연관 관계가 존재해야 한다.
다대일 모델(Many-to-One Model)
다대일 모델은 많은 사용자 수준 스레드를 하나의 커널 스레드로 사상한다.
스레드 관리가 사용자 공간의 스레드 라이브러리에 의해 행해지기 때문에 효율적이다.
하지만 한 스레드가 봉쇄형 시스템 콜을 할 경우, 전체 프로세스가 봉쇄된다.
또한 한 번에 하나의 스레드만이 커널에 접근할 수 있기 때문에, 다중 스레드가 다중코어 시스템에서 병렬로 실행될 수 없다.
일대일 모델 (One-to-One Model)
일대일 모델은 각 사용자 스레드를 각각 하나의 커널 스레드로 사상한다.
이 모델은 하나의 스레드가 봉쇄적 시스템 콜을 호출하더라도 다른 스레드가 실행될 수 있기 때문에 다대일 모델보다 더 많은 병렬성을 제공한다.
또한 다중 처리기에서 다중 스레드가 병렬로 수행되는 것을 허용한다.
이 모델의 단점은 사용자 수준 스레드를 생성할 때 그에 따른 커널 스레드를 생성해야 되기 때문에 오버헤드가 응용 프로그램의 성능을 저하시킬 수 있다.
그러므로 시스템에 의해 지원되는 스레드의 수를 제한해야 한다.
다대다 모델(Many-to-Many Model)
다대다 모델은 여러 개의 사용자 수준 스레드를 그보다 더 작은 수 혹은 같은 수의 커널 스레드로 멀티플렉스 한다.
이 모델은 필요한 만큼 많은 사용자 수준 스레드를 생성할 수 있고, 상응하는 커널 스레드가 다중 처리기에서 병렬로 수행될 수 있으며, 봉쇄형 시스템 콜이 발생해도 다른 스레드의 수행을 스케줄 할 수 있다.
두 수준 모델은 여러 개의 사용자 수준 스레드를 그보다 더 작은 수 혹은 같은 수의 커널 스레드로 멀티플렉스 시키지만 또한 한 사용자 스레드가 하나의 커널 스레드에만 연관되는 것을 허용한다.
사실 여기까지만 보면 다대다 모델이 여타 다른 모델에 비해 압도적으로 좋은 모델인 것 같았다.
하지만 “solaris 운영체제는 이전 버전에서는 다대다 모델을 지원하였으나 일대일 모델로 바뀌었다” 라는 구절을 발견했다.
그래서 그 이유를 찾아보니 스택오버플로우에서 이 이슈에 대해 답변이 올라온 걸 발견하였다.
(한글로 구글링하니 놀랍게도 나처럼 공룡책을 베껴다가 포스팅하는 자료만이 발견되었다…)
Let’s say you have 3 kernel mode “threads” and 10 user mode threads. If 4 user mode threads try to do I/O at the same time, the 4th thread will block until one of the first three threads complete.
일대일 모델의 병렬성이 그에 따른 오버헤드를 감안하더라도 값어치가 충분하다라는 뜻인 것 같다.
암묵적 스레딩은 스레딩의 생성과 관리 책임을 응용 개발자로부터 컴파일러와 실행시간 라이브러리에게 넘겨주는 것이다.
암묵적 스레딩을 이용하여 다중코어 처리기를 활용할 수 있는 다중 스레드 프로그램을 설계하는 접근법에는 스레드 풀이 있다.
스레드 풀의 기본 아이디어는 프로세스를 시작할 때 아예 일정한 수의 스레드들을 미리 풀로 만들어두는 것이다.
요청을 받을 때마다 무한정 새로운 스레드를 만들어준다면 CPU 시간, 메모리 공간과 같은 시스템 자원의 고갈 위험이 있다.
미리 만들어둔 스레드들은 평소에는 하는 일 없이 일감을 기다리다 요청이 들어오면 한 스레드에게 그것을 할당한다.
요청을 다 서비스해 주었으면 그 스레드는 다시 풀로 돌아가 다음 작업을 기다린다.
풀에 있는 스레드가 바닥나면 가용 스레드가 생길 때까지 대기한다.
이와 같은 스레드 풀은
-
새 스레드를 만들어 주기보다 기존 스레드로 서비스해 주는 것이 더 빠르다.
-
임의 시각에 존재할 스레드 개수에 제한을 둠으로 시스템에 가는 부담을 제어한다.
-
태스크 생성법과 태스크를 분리하면 태스크 실행 방법을 다르게 할 수 있다.
와 같은 장점을 가질 수 있다.