스레드
•
스레드는 CPU 이용의 기본 단위로, 스레드 ID와 프로그램 카운터(PC, Program Counter), 레지스터 집합, 스택으로 구성된다.
•
스레드는 같은 프로세스에 속한 다른 스레드와 코드 섹션과 데이터 섹션, 열려있는 파일들, signal 등 같은 운영체제 자원들을 공유한다.
•
프로세스는 기본적으로 최소 하나의 제어 스레드를 가지고 있고(단일 스레드), 다수의 제어 스레드를 가지고 동시에 하나 이상의 일을 처리할 수도 있다(다중 스레드).
1.
동기
•
현대의 컴퓨터와 모바일 기기에서 작동하는 거의 모든 소프트웨어 어플리케이션들은 다중 스레드를 사용한다. 다중 스레드는 여러 개의 실행 흐름(스레드)를 가진 독립적인 프로세스로 구현된다.
•
대표적인 예로, 웹 브라우저에서 하나의 스레드가 이미지나 텍스트를 표시하고 다른 스레드는 네트워크에서 데이터를 검색하고 있도록 할 수 있다.
•
대부분 운영체제의 커널도 일반적으로 다중 스레드로 구성되어 있다.
2.
장점
•
응답성(responsiveness)
◦
다중 스레드를 이용하면 응용 프로그램이 긴 작업을 수행 하더라도 동시에 다른 작업을 수행할 수 있기 때문에, 사용자에 대한 응답성을 증가 시킨다.
•
자원 공유(resource sharing)
◦
공유 메모리나 메세지 전달 방식을 통해서만 자원을 공유할 수 있는 프로세스와는 달리, 스레드는 해당 스레드가 속한 프로세스의 자원들과 메모리를 공유한다.
•
경제성(economy)
◦
프로세스 생성을 위해 메모리와 자원을 할당하는 것은 많은 비용이 들기 때문에, 프로세스의 자원을 공유하는 스레드를 생성하고 문맥 교환하는 것이 더 경제적이다. 또한 일반적으로 스레드는 프로세스보다 생성할 때 시간과 메모리를 덜 사용하고, 문맥 교환에서도 스레드가 프로세스보다 더 빠르게 동작한다.
•
규모 적응성(scalability)
◦
다중 스레드의 이점은 다중 처리기 구조에서 각기 다른 스레드가 병행적으로 수행될 수 있다는 점에 있다. 단일 스레드 프로세스는 처리기가 많더라도 오직 한 처리기에서만 실행된다.
다중 코어 프로그래밍
•
컴퓨터는 단일 CPU 시스템에서 다중 CPU 시스템으로, 다중 CPU 시스템에서도 CPU 내에 여러 개의 컴퓨팅 코어를 배치하는 방향으로 발전해왔다. CPU 내의 코어들은 운영체제에서는 각각 별개의 CPU처럼 보이는데, 이런 시스템을 다중 코어라고 부른다.
•
다중 코어 프로그래밍은 여러 개의 코어를 통해 각기 다른 스레드를 병렬적으로 수행하여 성능을 향상시키는 기법이다.
•
병행성 vs 병렬성
◦
병행성 : 모든 작업을 진행하여 둘 이상의 작업을 진행
◦
병렬성 : 둘 이상의 작업을 동시에 수행
◦
단일 코어 시스템에서는 병렬성 없이 병행성을 가질 수 있다.
•
단일 코어 시스템에서는 그림과 같이 한 번에 하나의 스레드만 실행시킬 수 있기 때문에, 여러 스레드를 병행적으로 실행시킬 수 있지만 병렬적으로 실행시킬 수는 없다.
•
멀티 코어 시스템에서는 그림처럼 각 코어에 별도의 스레드를 할당할 수 있기 때문에 병행성과 병렬성 모두를 가질 수 있다.
•
Amdahl’s Law
◦
Amdahl’s Law는 순차 실행 구성요소와 병렬 실행 구성요소의 어플리케이션에서 코어를 추가했을 때 얻을 수 있는 잠재적인 성능 이득을 계산하는 공식이다.
◦
N개의 처리 코어를 가진 시스템에서 반드시 순차적으로 실행되어야 하는 S개의 구성요소를 가진 시스템의 성능 이득은 다음과 같이 계산된다.
◦
예를 들면, 75%의 병렬 실행 구성요소와 25%의 순차 실행 구성요소를 가진 시스템이라면 코어가 2개일 때는 약 1.6배의 속도 향샹을, 4개의 코어를 가질 때는 2.28배의 속도 향상을 기대할 수 있다.
•
프로그래밍 도전과제
◦
운영체제를 설계할 때 여러 코어를 활용하는 스케줄링 알고리즘을 개발하여 병렬적으로 수행될 수 있도록 해야한다. 일반적으로 이러한 다중 코어 시스템을 프로그래밍하기 위해서는 5개의 극복해야할 도전 과제가 있다.
◦
태스트 인식(identifying tasks) : 어플리케이션 중 개별 코어에서 병렬적으로 실행가능한 영역을 찾아야한다.
◦
균형(balance) : 찾아낸 병렬적으로 수행할 수 있는 영역들이 전체 작업에 균등한 기여도를 가지도록 나누어야 한다.
◦
데이터 분리(data spliting) : 어플리케이션을 독립된 작업으로 나누는 것 처럼, 작업에서 접근하고 조작하는 데이터 또한 개별 코어에서 사용할 수 있도록 나누어져야한다.
◦
데이터 종속성(data dependency) : 작업 수행 중 접근하는 데이터가 둘 이상의 작업에서 종속성이 없는지 검토되어야 한다.
◦
시험 및 디버깅(testing and debuging) : 프로그램이 정상적으로 실행이 되는지 시험하고 디버깅 해보아야 한다.
•
병렬 실행의 유형
◦
일반적으로 병렬 실행에는 데이터 병렬 실행과 테스크 병렬 실행 두 가지 유형이 존재한다.
◦
데이터 병렬 실행 : 동일한 작업을 동시에 다른 데이터에 대해 수행하는 것으로, 데이터 병렬 실행은 데이터의 독립성을 가정하고 데이터 간의 연산이 동시에 진행되는 것을 목표로 한다.
◦
테스트 병렬 실행 : 여러 개의 작업을 동시에 실행하는 것으로, 주어진 작업을 더 작은 작업 단위로 분할하고, 각 작업을 별도의 스레드 또는 프로세스에서 실행하여 동시에 진행한다.
◦
이 두 실행 방법은 혼합하여 사용될 수 있다.
다중 스레드 모델
•
스레드는 사용자 레벨에서 사용자 스레드(User Thread), 커널 수준에서 커널 스레드(Kernel Thread)가 제공된다.
•
다대일 모델(Many-to-One Model)
◦
다대일 모델은 여러 개의 사용자 스레드를 하나의 커널 스레드로 매핑하여 사용하는 방식이다.
◦
문맥 교환 시 커널 스레드가 개입을 하지 않기 때문에 빠르다.
◦
멀티 코어를 활용하지 못하고 Blocking 방식에서 하나의 스레드가 Block 되면 모든 스레드가 멈춘다.
◦
Java에서 초기에 사용된 그린 스레드 개념도 이 모델을 사용하였다.
•
일대일 모델(One-to-One Model)
◦
일대일 모델은 하나의 사용자 스레드를 하나의 커널 스레드로 매핑하여 사용하는 방식이다.
◦
각 커널 스레드가 독립적으로 존재하기 때문에 멀티 코어를 활용할 수 있고, Blocking 방식에서 하나의 스레드가 Block이 되더라도 다른 스레드는 잘 동작할 수 있다.
◦
스케줄링을 커널이 수행하기 때문에 문맥 교환이 느리다.
•
다대다 모델(Many-to-Many Model)
◦
다대일 모델과 일대일 모델의 단점을 보완하기 위해 혼합하여 사용하는 방식이다.
◦
다대일 모델과 일대일 모델의 장점을 모두 가지지만, 구현이 어렵다는 단점이 있다.
◦
다대다 모델에서 부분적으로 일대일 모델을 적용하는 두 수준 모델(two-level model)이라는 변형 방식도 있다.
스레드 라이브러리
•
스레드 라이브러리는 프로그래머에게 스레드를 생성하고 관리하기 위한 API를 제공하는 라이브러리이다.
•
스레드 라이브러리는 두 가지 종류가 있는데, 커널의 지원 없이 사용자 스레드만을 제공하는 라이브러리와 커널 스레드를 생성할 수 있는 커널 수준의 라이브러리이다.
•
비동기 스레딩은 부모가 자식을 생성한 후 부모는 자신의 실행을 재개하여 부모와 자식 스레드가 서로 독립적으로 병행하게 실행되는 방식이다.
•
동기 스레딩은 부모에서 자식 스레드를 생성한 후 자식 스레드가 모두 종료할 때까지 기다렸다가 자신의 실행을 재개하는 방식이다.
•
대표적인 스레드 라이브러리로는 POSIX의 Pthread, Windows 스레드 라이브러리, Java 스레드 라이브러리가 있다.
암묵적 스레딩
•
수많은 스레드를 가진 응용 프로그램을 설계하는 일은 쉽지 않은데, 그런 어려움을 극복하기 위해 스레딩의 생성과 관리를 컴파일러와 라이브러리에게 넘기는 것이 암묵적 스레딩이라 한다.
1.
스레드 풀
•
스레드가 필요한 시스템에서 요청을 받을 때마다 스레드를 만들게 되면, 스레드를 생성할 때 소요되는 시간과 시스템에서 동시에 실행 시킬 수 있는 최대 스레드의 개수 제한으로 인해 한계가 있다.
•
스레드 풀(pool) 방식은 이러한 단점을 극복하기 위해 프로세스를 시작할 때, 일정한 수의 스레드를 미리 만들어두고 요청을 받으면 풀에서 꺼내 할당하는 방식이다.
•
새 스레드를 생성하는 것보다 이미 생성된 스레드를 할당하는 것이 일반적으로 더 빠르고, 스레드 풀에 존재하는 스레드 수로 특정 시각에 존재하는 전체 스레드의 개수를 제한할 수 있다는 장점이 있다.
•
스레드 풀에 있는 스레드의 개수는 CPU 수와 메모리 용량, 최대 동시 요청 클라이언트 수 등을 고려하여 결정해야한다. 추가적으로 시스템 부하에 따라 스레드 풀의 스레드 개수를 동적으로 조정하여 유연하게 대처할 수도 있다.
2.
Fork Join
•
fork-join 모델은 부모 스레드가 하나 이상의 자식 스레드를 생성(fork)하고 자식의 종료를 기다린다(join).
•
이 모델은 명시적 스레드 생성 방식이지만, fork 단계에서 스레드가 직접 구축되지 않고 병렬 작업으로 하는 방식으로 암시적 스레딩으로 사용할 수도 있다.
3.
OpenMP
•
OpenMP 방식은 C나 C++, FORTRAN으로 작성된 API와 컴파일러 디렉티브의 집합이다. OpenMP는 공유 메모리 환경에서 병렬로 실행될 수 있는 블록인 병렬 영역(parallel regions)을 찾아 컴파일러 디렉티브를 삽입한다. 컴파일러 디렉티브는 OpenMP RunTime 라이브러리에 해당 영역들을 병렬로 실행하라고 지시한다.
•
OpenMP는 병렬화를 위한 디렉티브를 제공할 뿐만 아니라 직접 필요한 스레드 개수를 정하는 것처럼 병렬화 수준을 선택하여 정할 수 있다.
4.
Grand Central Dispatch
•
Grand Central Dispatch(GCD)는 macOS와 iOS 운영체제를 위해 Apple에서 개발한 기술로, 병렬로 실행될 코드 섹션(태스크)를 식별할 수 있도록 하는 RunTime 라이브러리와 API 등을 제공한다. OpenMP와 마찬가지로 GCD는 스레딩의 대부분 세부사항들을 관리한다.
5.
Intel 스레드 빌딩 블록
•
Intel Thread Building Block(TBB)는 C++에서 병렬 응용 프로그램 설계를 지원하는 템플릿 라이브러리로, 특별한 컴파일러나 언어 지원이 필요하지 않다.
스레드와 관련된 문제들
1.
fork() 및 exec() 시스템 콜
•
fork()를 호출하는 경우 새로운 프로세스는 프로세스 전체를 복사하는 경우와 해당 스레드만 복제하여 프로세스를 만드는 경우 두 가지가 있다. 일부 UNIX에서만 이 두가지 버전을 모두 제공한다.
•
exec()를 호출하게 되면 복제된 프로세스가 대체되는데, 이런 경우에는 프로세스에서 fork를 할 때 모든 스레드를 복사할 필요가 없다. 하지만 exec를 하지 않는다면 모든 스레드를 복사할 필요가 있을 수 있다.
2.
신호 처리
•
신호(signal)은 프로세스에서 특정 이벤트가 일어났음을 알려주기 위해 사용되는데, 동기식으로 전달될 수도 있고 비동기식으로 전달될 수도 있다.
•
모든 신호마다 커널이 실행시키는 디폴트 신호 처리기가 존재하는데, 이 디폴트 신호처리기는 사용자 정의 처리기에 의해 대체 될 수 있다.
•
멀티 스레드 시스템에서 신호 처리는 해당 신호가 어느 스레드에 전달되어야 하는가에 대한 문제가 있다.
3.
스레드 취소
•
스레드 취소(thread cancellation)은 스레드가 끝나기 전에 강제 종료시키는 작업을 말한다. 스레드 취소 작업에서 취소되어야 하는 스레드를 목적 스레드(target thread)라 부른다.
•
비동기식 취소(asynchronous cancellation) : 하나의 스레드가 목적 스레드를 바로 강제 종료하는 방식이다.
•
지원 취소(deferred cancellation) : 목적 스레드가 주기적으로 자신이 강제 종료 되어야 할지 점검하여 종료되는 방식이다.
•
비동기식으로 취소를 할 경우 목적 스레드에 할당된 시스템 자원을 회수하지 못하는 경우가 발생할 수 있다.
•
pthread에서 기본적으로 옵션으로 지연 취소가 설정되어 있는데, 목적 스레드가 취소 점에 도달한 경우에 취소가 발생한다. 스레드가 취소될 때 정리 핸들러(cleanup handler) 함수를 호출되게 해서 스레드가 종료되기 전에 할당된 자원을 해제할 수 있다.
4.
스레드-로컬 저장장치
•
하나의 프로세스 내 모든 스레드는 해당 프로세스의 데이터를 공유하는데, 상황에 따라 스레드가 자신만 접근할 수 있는 데이터를 가져야할 수 있다. 이런 데이터를 저장할 수 있는 공간을 스레드-로컬 저장장치(TLS, thread-local storage)라고 부른다.
•
예를 들면, 트랜잭션 처리 시스템에서 하나의 트랜잭션을 하나의 스레드가 처리한다면 스레드마다 스레드 국지 저장소가 있어야 한다.
•
TLS는 지역 변수와 비슷한 것처럼 보이는데, 지역 변수는 하나의 함수가 호출되는 동안에만 보이지만 TLS는 전체 함수에서 보인다.
5.
스케줄러 액티베이션
•
스레드 라이브러리와 커널 스레드 간의 통신 조정은 응용 프로그램이 최고의 성능을 보장하기 위해 커널 스레드 수를 동적으로 조절하는 것이다.
•
다대다 모델이나 두 수준 레벨 모델을 구현하는 시스템에서 사용자 스레드와 커널 스레드 사이에 커널 스레드에 부속되어 커널 스레드를 스케줄하는 경량 프로세스(Light Weight Process)가 존재한다.
•
사용자 스레드와 커널 스레드 간의 통신 방법 중 하나로 스케줄러 액티베이션이 있다. 스케줄러 액티베이션은 커널에 LWP의 집합을 제공하고 사용자 스레드를 가상 처리기로 스케줄링하여 동작한다.
운영체제 사례
1.
Windows 스레드
•
Windows 어플리케이션은 프로세스 형태로 실행되며, 일대일 대응 스레드 방식을 사용하여 프로세스에 하나 이상의 스레드를 가진다.
•
ETHREAD, KTHREAD, TEB의 자료 구조를 가진다.
◦
ETHREAD : 실행 스레드 블록
◦
KTHREAD : 커널 스레드 블록
◦
TEB : 스레드 환경 블록
•
ETHREAD, KTHREAD는 모두 커널 안에 존재하고, TEB는 사용자 모드에서 실행될 때 접근되는 사용자 공간 구조이다.
2.
Linux 스레드
•
Linux에서는 fork() 시스템 콜을 호출하여 프로세스를 생성할 수 있고 clone() 시스템 콜을 통해 스레드를 생성할 수 있지만, Linux 자체는 프로세스와 스레드를 구분하지않는다. 프로그램 내의 제어 흐름을 나타내기 위해 프로세스나 스레드보다 태스크라는 용어를 주로 사용한다.
•
그림과 같이 clone()이 호출 될 때 자식 태스크가 자료구조를 얼마나 공유할 지를 결정하는 플래그 집합이 전달된다.
•
fork()가 호출되면 부모 프로세스의 자료구조를 복사하여 생성하고, clone()을 호출하게 되면 전달된 플래그에 따라 부모 태스크의 자료 구조를 가리키게 된다.