API for Pthreads, Windows, Java thread libraries
implicit threading을 제공하는 몇 가지 전략
multithreaded programming과 관련된 문제
Windows와 Linux에서 threads 대한 운영체제 지원
* Overview
- thread는 CPU utilization의 기본 단위이다; 그것은 thread ID, program counter, register set, stack으로 구성된다. 그것은 다른 threads들과 함께 같은 프로세스에 속하여 그것의 code section, data section, 그리고 open files and signals같은 다른 운영체제 자원들을 공유한다. 전통적인(또는 무거운) process는 단일 쓰레드 컨트롤을 가진다. 만약 한 프로세스가 여러 쓰레드 제어를 가진다면, 그것은 한 번에 한 task 이상을 수행할 수 있다.
- 현대 컴퓨터에서 작동하는 대부분의 소프트웨어 어플리케이션들은 multithreaded이다. 한 어플리케이션은 일반적으로 몇 가지 쓰레드 제어와 함께 별개의 프로세스로서 구현된다. 한 웹 브라우저는 한 쓰레드는 이미지 또는 테스트를 보이게 하고, 또 다른 쓰레드가 네트워크로부터 데이터를 가져오게 한다. 어플리케이션들은 또한 멀티코어 시스템에서 프로세싱 능력을 이용하도록 설계될 수 있다. 그러한 어플리케이션들은 여러 컴퓨팅 코어를 거쳐 병렬로 몇 가지 CPU집중적인 일들을 수행할 수 있다.
- 여러 사용자 접근을 요청받는 웹 서버의 경우, 요청을 한 단일 프로세스에서 받도록 하고, 요청을 받을 때, 서버가 그 요청을 서비스하는 별개의 프로세스를 만들기도 한다. 이 process-creation method는 threads가 인기있어지기 전에 흔히 사용되었다. 그러나, Process creation은 time consuming이고 resource intensive하다. 만약 그 새로운 프로세스가 존재하는 프로세스와 같은 일을 수행한다면, 왜 그러한 overhead를 초래하는가? 여러 쓰레드들을 포함하는 한 프로세스를 사용하는것이 일반적으로 더 효율적이다. 만약 web-server process가 multithreaded 라면, 그 서버는 client requests를 듣는 별개의 thread를 만들 것이다. 한 요청이 왔을 때, 또 다른 프로세스를 만들기 보다는, 그 서버는 그 요청을 서비스 하는 새로운 쓰레드를 만들고 추가적인 요청에 대해 듣는 것을 재개할 것이다.
- Threads는 remote procedure call (RPC) systems에서 또한 중요한 역학을 한다. RPC가 ordinary function or procedure calls와 유사한 통신 메커니즘을 제공하여 프로세스 간 통신을 허용하는 것을 기억해라. 일반적으로 RPC servers는 multithreaded 이다. 한 서버가 메세지를 받을 때, 그것은 별개의 쓰레드를 사용하여 그 메세지를 서비스한다. 이것은 그 서버가 몇 가지 동시 요청들을 서비스하도록 한다.
- 마지막으로 대부분의 운영체제 커널들은 이제 multithreaded 이다. 몇 가지 threads들은 kernel에서 작동하고, 각 thread는 특정한 task를 수행한다. 장치를 관리하고, 메모리를 관리하고 interrupt handling 하고. 예를들어, Solaris는 interrupt handling에 대해 구체적으로 커널에 한 쓰레드들의 집합을 가진다. Linux는 그 시스템에서 free memory의 양을 관리하기 위해 kernel thread를 사용한다.
- multithreaded programming의 장점은 4개의 주된 카테고리로 나눠질 수 있다:
- Responsiveness : 한 interactive application을 Multithreading하는 것은 한 프로그램이 그것의 일부가 봉쇄되거나 긴 연산을 수행하고 있을지라도 계속 작동하게 허용할지도 모른다. 그것으로 인해 그 사용자에 대해 응답성을 높인다. 이 특징은 특히 상요자 interfaces를 설계할 때 유용하다.
- Resource sharing : 프로세스들은 shared memory와 message passing같은 기법들을 통해서만 자원들을 공유할 수 있다. 그러한 기법들은 프로그래머에 의해서 명시적으로만 배치되어야 한다. 그러나, 쓰레드들은 그들이 기본적으로 속한 프로세스의 메모리와 자원을 공유한다. 코드와 데이터를 공유하는 것의 이점은 그것은 한 어플리케이션이 같은 주소 공간 내에서 다른 활동하는 쓰레드들을 가지게 한다.
- Economy : process creation를 위해 메모리와 자원을 할당하는 것은 비싸다. threads가 그것들이 속하는 프로세스의 자원을 공유하기 때문에, threads를 생성하고 context-switch하는 것이 좀 더 실속이 있다. 경험적으로 overhead에서 차이를 추측하는 것은 어려울 수 있지만, 일반적으로 쓰레드보다 프로세스를 생성하고 관리하는 것이 좀 더 크게 일반적으로 시간을 소모한다. Solaris에서 예를들어, 프로세스를 생성하는 것이 쓰레드를 생성하는 것 보다 30배 느렸고, context switching은 5배 더 느렸다.
- Scalability : multithreading의 장점들은 multiprocessor architecture에서 더욱 클 수 있다. 거기에서 threads들은 다른 프로세싱 코어에서 병렬로 작동할지도 모른다. single-threaded process는 얼마나 많은 것들이 이용가능한지와 상관없이 오직 한 프로세서에서만 작동할 수 있다.
* Multicore Programming
- 더 많은 연산 성능에 대한 필요성의 반응으로 컴퓨터 디자인 역사 초기에 단일 CPU 시스템은 multi-CPU 시스템으로 진화했다. 시스템 설계에서 좀 더 최근의 비슷한 트렌드는 여러 컴퓨팅 코어를 단일 칩에 놓는 것이다. 각 코어는 운영체제에게 별개의 프로세서로서 나타난다. 그 코어들이 CPU 칩들에 걸쳐서 또는 CPU 칩들 내에서 나타나든, 우리는 이러한 시스템을 multicore or multiprocessor systems라고 부른다. Multithreaded programming은 이러한 multiple computing cores의 효율적인 사용관 개선된 concurrency을 위한 메커니즘을 제공한다.
- single computing core만 있는 시스템에서, concurrency은 단지 쓰레드들의 실행이 시간에 따라 끼워지는 것을 의미한다. 왜냐하면 그 프로세싱 코어가 한 번에 한 쓰레드 만을 실행할 수 있기 때문이다. 그러나, 여러개의 코어를 가진 시스템에서, concurrency는 그 쓰레드들이 병렬로 작동할 수 있다는 것을 의미한다. 왜냐하면 그 시스템은 별 개의 쓰레드를 각 코어에 할당할 수 있기 때문이다.
- parallelism(병렬성)과 concurrency(병행성)의 구분에 주의해라. 만약 한 시스템이 한 개 이상의 task를 동시에 할 수 있다면, 그 시스템은 parallel이다. 대조적으로 concurrent system은 모든 tasks가 진행되게하여 한 개 이상의 task를 진행한다. 따라서, 병렬성 없이 병행성을 갖는 것이 가능하다.
- 일반적으로 5개의 영역이 멀티코어 시스템의 프로그래밍에서의 어려움을 보여준다:
- 1. Identifying tasks : 이것은 별개이고, 병행할 수 있는 tasks로 나눠질 수 있는 영역을 찾기 위해 어플리케잇녀을 조사하는 것을 포함한다. 이상적으로 tasks들은 서로에게 독립적이고 따라서 개별 코어에서 병렬로 실행할 수 있다.
- 2. Balance : 병렬로 작동할 수 있는 tasks를 확인하는 동안, 프로그래머들은 또한 그 tasks가 동일한 값에 대해 동이한 작업을 수행하는지 보장해야 한다. 어떤 사례에서, 어떤 task는 다른 tasks 만큼 전반적인 프로세스에 많은 가치를 기여하지 않을지도 모른다. 그 task를 작동하기 위해 별개의 실행 코어를 사용하는 것은 비용에 대한 가치가 없을지도 모른다.
- 3. Data splitting : 어플리케이션들이 별개의 tasks로 나눠지듯이, tasks에 의해 접근되고 조작되는 데이터는 별개의 코어들에서 작동되도록 분할되어야 한다.
- 4. Data dependency : tasks에 의해 접근되는 data는 두 개 이상의 tasks 사이의 의존성을 위해 검사되어야 한다. 한 task가 다른 것의 data를 의존할 때, 프로그래머는 그 tasks의 실행이 data dependency를 수용하도록 동기화되는 것응ㄹ 보장해야 한다.
- Testing and debugging : 한 프로그램이 여러 코어들에서 병렬로 작동할 때, 많은 다른 실행 경로들이 가능하다. 그러한 concurrent programs을 테스트하고 디버깅하는 것은 single-threaded applications을 테스트하고 디버깅하는 것 보다 내재적으로 더 어렵다.
- 일반적으로 두 가지 종류의 parallelism이 있다: data parallelism and task parallelism. Data parallelism은 여러 컴퓨팅 코어에 걸쳐 같은 데이터의 subsets을 분산하고, 각 코어에서 같은 연산을 수행하는 것에 집중한다. Task parallelism은 여러 컴퓨팅 코어에 걸쳐 data가 아닌 tasks (threads)를 분산하는 것을 포함한다. 각 쓰레드는 유일한 연산을 수행한다. 다른 쓰레드들은 같은 데이터에 대해 연산할지도 모르고 또는 그것들은 다른 데이터에 연산할지도 모른다. 기본적으로 data parallelism은 여러 코어에 걸쳐 data의 분산을 포함하고, task parallelism은 여러 코어에 걸쳐 task의 분산을 한다. 그러나 실제로, 어떠한 어플리케이션도 둘 중 하나의 parallelism만을 엄격히 따르지 않는다. 대부분의 경우에 어플리케이션들은 이러한 두 전략을 혼합하여 사용한다.
* Multithreading Models
- threads에 대한 지원은 user level에서 user threads를 위해서 제공되거나 kernel에 의해 kernel threads를 위해서 제공되는 둘 중 하나이다. User threads는 kernel 위에서 지원되고, kernel support 없이 관리된다. 반면에, kernel threads는 운영체제에 의해 지원되고 직접 관리된다. 가상적으로 모든 현대 운영체제들은 kernel threads를 지원한다. 궁극적으로 user threads와 kernel threads 사이의 관계가 존재한다. 세 가지 방식으로 그것을 볼 수 있다 : many-to-one model, one-to-one model, many-to-many model.
- many-to-one model은 많은 user-level threads를 한 kernel thread에 매핑한다. Thread management는 user space에 있는 thread library에 의해 처리되고, 그래서 그것은 효율적이다. 그러나 그 전체 process는 만약 한 thread가 blocking system call을 한다면 block될 것이다. 또한 오직 한 thread만이 한 번씩 kernel에 접근할 수 있기 때문에, 여러 threads들은 multicore systems에서 병렬로 실행될 수 없다. Solaris systems에서 이용가능한 thread library이고, Java의 초기 버전에 채택된 Green threads는 many-to-one model을 사용했다. 그러나, 거의 시스템들이 여러 프로세싱 코어를 이용하지 못하는 것 때문에 그 모델을 사용하지 않는다.
- one-to-one model은 각 user thread를 한 개의 kernel thread에 매핑한다. 그것은 한 thread가 blocking system call을 할 때 또 다른 thread가 작동하는 것을 허용하여 many-to-one model보다 더 많은 concurrency를 제공한다. 그것은 또한 여러 쓰레드가 multiprocessors에서 병렬로 실행되는 것을 허용한다. 이 모델에 대한 유일한 단점은 user thread를 만드는 것이 대응되는 kernel thread를 만드는 것을 요구한다는 것이다. kernel threads를 만드는 것의 overhead가 한 어플리케이션에 성능에 부담을 줄 수 있기 때문에, 대부분의 이모델에 대한 구현은 그 시스템에 의해 지원되는 쓰레드의 개수를 제한한다. 윈도우즈 운영체제와 함께 Linux는 one-to-one model을 구현한다.
- many-to-many model은 많은 user-level threads를 더 작은 또는 동등한 수의 kernel threads에 연결한다. kernel threads의 개수는 특정한 어플리케이션 또는 특정한 기계에 구체적일지도 모른다 (한 어플리케이션은 싱글 프로세서보다 멀티프로세서에서 더 많은 kernel threads를 할당 받을지도 모른다).
- concurrency에 대해서 고려해보자. many-to-one model은 그 개발자가 바라는 만큼의 많은 user threads를 만들 수 있게 해준다. 그것은 진정한 concurrency를 만들지 않는데, 왜냐하면 그 kernel이 한 번에 한 thread만을 schedule할 수 있기 때문이다. one-to-one model은 더욱 큰 concurrency를 허용하지만, 그 개발자는 한 어플리케이션 내에서 너무 많은 threads를 생성하지 않도록 신경써야 한다. many-to-many model은 이러한 단점들 중 어떠한 것도 겪지 않는다: 개발자는 필요한 만큼 많은 threads를 만들 수 있고, 그에 대응되는 kernel threads는 multiprocessor에서 병렬로 실행될 수 있다. 또한, 한 thread가 blocking system call을 수행할 때, 그 kernel은 실행을 위한 또 다른 thread를 schedule 할 수 있다.
- many-to-many model에 대한 한 가지 변형은 여전히 많은 user-level threads가 더 작거나 같은 수의 kernel thread에 연결되게 할 뿐만 아니라, user-level thread 하나가 한 kernel thread에 제한되도록 한다. 이 변형은 가끔씩 two-level model로 언급된다.
* Thread Libraries
- thread library를 구현하는 것의 두 가지 주요한 방법들이 있다. 첫 번째 접근법은 kernel support 없이 user space에서 library를 전적으로 제공한느 것이다. 그 라이브러리에 대해 모든 코드와 데이터 구조들은 user space에서 존재한다. 이것은 그 라이브러리에 있는 한 함수를 불러오는 것인 user space에서 system call이 아닌 local function call을 만든다.
- 두 번째 접근법은 운영체제에 의해 직접 지원되는 kernel-level library를 구현하는 것이다. 이 경우에, 그 라이브러리에 대한 코드와 데이터 구조는 kernel space에서 존재한다. 그 라이브러리에 대해 API에서 한 함수를 불러오는 것은 kernel에 대한 system call을 만들어낸다.
- 세 개의 주된 tread libraries가 오늘날 사용된다: POSIX Pthreads, Windows, and Java. POSIX 표준 threads extension인 Pthreads는 user-level or kernel-level library 둘 중 하나로 제공될지도 모른다. Windows thread library는 Windows systems에서 이용가능한 kernel-level library이다. Java thread API는 threads가 Java programs에서 직접 생성되고 관리되도록 한다. 그러나, 대부분의 경우에 JVM은 host operating system위에 작동하기 때문에, Jav thread API는 일반적으로 host system에서 이용가능한 thread library를 이용하여 구현된다. 이것은 Windows systems에서 Java threads가 일반적으로 Windows API를 사용하여 구현되는 것을 의마한다; UNIX와 Linux systems은 종종 Pthreads를 사용한다.
- POSIX와 Windows threading에 대해, 전역으로 선언된 어떤 데이터 - 즉, 어떤 함수 밖에 선언된 -는 그 같은 프로세스에 속하는 모든 쓰레드들 사이에서 공유된다. Java는 전역 데이터에 대한 개념이 없기 때문에, 공유 데이터에 대한 접근은 threads 사이에 explicitly하게 정리되어야 한다. 한 함수에 지역으로 선언된 데이터는 일반적으로 stack에 저장된다. 각 쓰레드는 그것 자신의 stack을 가지고 있기 때문에, 각 thread는 그것 자신의 local data의 복사본이다.
- 여러 threads를 생성하는 것에 대한 두 가지 일반적인 전략이 있다 : asynchronous threading과 synchronous threading. asynchronous threading으로, 그 부모가 child thread를 생성한다면, 그 부모는 그것의 실행을 재개한다. 이것은 그 부모와 자식이 병행하게 실행되도록 하기 위해서이다. 각 thread는 모든 다른 쓰레드에 독립하여 실행되고, 그 부모 thread는 그것의 child가 언제 종료했느지를 알 필요가 없다. 그 threads가 독립적이기 때문에, 쓰레드들 사이에 어떠한 data sharing도 없다. 비동기 쓰레딩은 multithreaded server에 사용되는 전략이다.
- Synchronous threading은 부모 thread가 한 개 이상의 자식을 생성하고 부모가 다시 재개하기전에 모든 그것의 자식들이 종료하기를 기다려야만 할 때 발생한다. 이것은 소위 fork-join 전략이다. 여기에서 그 부모에 의해 생성된 쓰레드들은 병행하여 작업하지만, 그 부모는 이 작업이 완료될 때 까지 계속할 수 없다. 각 쓰레드가 그것의 일을 마친다면, 그것은 종료하고 그것의 부모에 join한다. 모든 자식들이 joined한 후에만, 그 부모는 실행을 재개할 수 있다. 일반적으로 synchronous threading은 쓰레드들 사이의 중요한 data sharing을 포함한다. 예를들어, 그 부모 thread는 그것의 여러 자식들에 의해 계산된 결과들을 합친다.
* Implicit Threading
- Multithreaded programming의 어려움을 다루고 프로그램의 설계를 더 잘 지원하는 한 가지 방법은 threading의 생성과 관리를 어플리케이션 개발자에서 컴파일러와 런타임 라이브러리로 넘기는 것이다. implicit threading이라고 용어가 지어진 이 전략은 오늘날 인기있는 트렌드이다.
- 위에서 multithreaded web server의 예시를 보여주었는데, 그 서버는 요청을 서비스하기 위해 별개의 쓰레드를 만든다. 별개의 쓰레드를 만드는 것이 별개의 프로세스를 만드는 것보다 훌륭하지만, 잠재적 문제를 갖는다. 첫 번째 문제는 쓰레드를 만드는 시간의 양이다. 게다가 쓰레드가 그것의 일을 완료되고나면은 버려질 것이다. 두 번째 문제는 좀 더 문제가 되는건데, 만약 우리가 모든 concurrent requests가 새로운 쓰레드에서 서비스되도록 한다면, 식스템에 병행하여 작동중이 쓰레드들의 수에 제한을 두지 않았다. 그래서 제한되지 않는 threads들은 CPU time or memory같은 시스템 자원들을 다 소모시킬 것이다. 이것에 대한 솔루션이 thread pool을 사용하는 것이다.
- thread pool뒤에 있는 일반적인 아이디어는 프로세스가 시작할 때 많은 쓰레드들을 만들어 놓고, 그것들을 한 pool에 두는 것이다. 거기에서 그것들은 쉬고 일을 기다린다. 서버가 한 요청을 받을 때, 그것은 이 pool에서 한 쓰레드를 깨우고 - 만약 이용가능하다면 - 그리고 그것을 서비스에 대한 요청에 넘긴다. 그 쓰레드가 그것의 서비스를 완료했다면, 그것은 pool로 돌아와서 좀 더 많은 일들을 기다린다. 만약 그 pool이 어떠한 이용가능한 thread가 없다면, 그 서버는 하나가 생길 때까지 기다려야 한다. Thread pools은 이러한 장점들을 제공한다:
- 존재하는 쓰레드로 요청을 서비스하는 것은 한 쓰레드를 만들기 위해 기다리는 것 보다 더 빠르다.
- thread pool은 어떤 한 지점에서 존재하는 쓰레드들의 개수를 제한한다. 이것은 특히 많은 수의 concurrent threads를 지원할 수 없는 시스템에 중요하다.
- 수행될 task를 task를 생성하는 기제와 분리하는 것은 우리가 task를 작동시키는데 다른 전략을 사용할 수 있게 해준다. 예를들어, 그 task는 한 time delay후에 실행되거나 주기적으로 실행되도록 스케쥴링 될 수 있다.
- pool에 있는 threads의 개수는 heuristically하게 시스템의 CPU의 개수, 물리 메모리의 양, concurrent client requests의 예상되는 개수 같은 요인들을 기반으로 설정될 수 있다. 좀 더 정교한 thread-pool 아키텍쳐는 동적으로 사용 패턴에 따라 쓰레드의 개수를 조절할 수 있다.
- OpenMP는 shared-memory environments에서 병렬 프로그래밍에 대한 지원을 제공하는 C, C++, FORTRAN으로 쓰여진 프로그램을 위한 API뿐만 아니라 compiler directives의 집합이다. OpenMP는 parallel regions을 병렬로 작동할 코드의 블럭으로 확인한다. 어플리케이션 개발자는 compiler directives를 그것들의 코드로 parallel regions으로 넣는다. 이러한 directives는 OpenMP run-time library가 그 영역을 병렬로 실행하도록 지시한다.
- Apple의 Mac OS X과 iOS 운영체제를 위한 기술인 Grand Central Dispatch (GCD)는 C언어에 대한 확장의 조합이고, API이며, 어플리케이션 개발자가 병렬로 실행되는 코드의 부분을 확인하도록 해주는 run-time library이다. GCD는 run-time execution을 위해 blocks을 dispatch queue에 넣어 스케쥴링한다. 그것이 한 큐에서 block을 제거할 떄, 그것은 그 블럭을 그것이 관리하는 thread pool에서 이용가능한 thread를 할당한다. GCD는 두 가지 종류의 dispatch queues를 확인한다 : serial과 concurrent. serial queue에 넣어진 Blocks은 FIFO 순서로 제거된다. 한 block이 그 큐에서 제거된다면, 그것은 또 다른 블럭이 제거되기전에 실행을 완료해야 한다. 각 프로세스는 그것 자신의 serial queue (main queue라고 알려진)를 가지고 있다. 개발자는 특정 프로세스들에 local인 추가 serial queues를 생성할 수 있다. Serial queue는 몇 가지 tasks의 순차적 실행을 보장하는데 유용하다.
- concurrent queue에 배치된 Blocks은 FIFO order로 제거되지만, 몇 가지 blocks은 한 번에 제거될지도 모른다. 따라서 여러 blocks이 병렬로 실행되는 것을 허용한다. 세 개의 system-wide concurrent dispatch queues가 있고, 그것들은 우선순위에 따라 분류된다: low, default, and high. 우선순위들은 blocks의 상대적인 중요성의 근사를 나타낸다.
- 내부적으로 GCD의 thread pool은 POSIX threads로 구성된다. GCD는 동적으로 pool을 관리하고, 그 쓰레드들의 수가 어플리케이션의 수요와 시스템 capacity에 따라 늘어나고 줄어들게 한다.
* Threading Issues
- fork()는 Chapter 3에서 별개의, 복제된 process를 생성하는데 사용되는 system call이다. fork()와 exec() system calls의 의미는 multithreaded program에서 변한다. 만약 한 프로그램에서 한 thread가 fork()를 한다면, 그 새로운 프로세스는 모든 쓰레드들을 복사는가? 또는 그 새로운 프로세스는 single-threaded인가? 어떤 UNIX systems은 for()의 두 버전을 갖는 것을 선택한다. 하나는 모든 쓰레드들을 복제하는 것 그리고, fork() system call을 불러냈던 쓰레드 만을 복제하는 다른 것. exec() system call은 일반적으로 Chapter 3에서 설명했던 것과 같은 방식으로 작동한다. 즉, 만약 한 쓰레드가 exec() system call을 불러낸다면, exec()에 대한 파라미터에 명시된 그 프로그램은 전체 프로세스를 대체할 것이다 - 전체 쓰레드들을 포함하여. fork()의 둘 중 어떤 버전이 사용될지는 application에 달려있다. 만약 exec()가 forking이후에 즉시 호출된다면, 그러면 모든 쓰레드들을복제하는 것은 불필요하다. 왜냐하면 exec()에 대한 파라미터에 명시된 프로그램이 그 프로세스를 대체할 것이기 때문이다. 이 사례에서, 오직 그 호출한 thread만을 복제하는 것이 적절하다. 그러나, 만약 그 별개의 process가 forking한후에 exec()를 호출하지 않는다면, 그별개의 프로세스는 모든 쓰레드들을 복제 해야한다.
- signal은 한 프로세스에게 특별한 이벤트가 발생했다고 알리기 위해 UNIX systems에서 사용된다. signal은 동기/비동기적으로 둘 중 하나로 받아질 지도 모른다, 신호받는 이벤트의 source와 그것에 대한이유에 따라. 동기든 비동기든, 모든 신호들은 다음과 같은 패턴을 따른다:
- 1. 한 signal은 특정한 이벤트의 발생에 의해 생성된다.
- 2. 그 signal은 한 process에 전해진다.
- 3. 일단 전해진다면, 그 signal은 처리되어야만 한다.
- synchronous signal의 예제들은 illegal memory access와 division by 0을 포함한다. Synchronous signals은 그 signal을 발생시켰던 연산을 수행한 같은프로세스에 전달된다. (synchronous라고 고려되는 이유이다). 한 signal이 작동하는 프로세스 외부의 사건에 의해 발생했을 때, 그 프로세스는 그 signal을 비동기적으로 받는다. 그러한 signals의 예는 한프로세스를 특정한 keystrokes로 (control C) 종료하는 것과 timer가 기한이 다 되게 하는 것을 포함한다. 일반적으로 비동기 signal은 또 다른 프로세스에 보내진다.
- 한 signal은 두 가지 가능한 handlers중의 하나에 의해 처리될지도 모른다 : 1. default signal handler. 2. user-defined signal handler. 모든 signal은 그 signal을 다룰 때 kernel이 작동시키는 default signal handler를 가지고 있다. 이 default action은 그 signal을 처리하기위해 호출되는 user-defined signal handler에 의해 overriden 될 수 있다. Signals들은 다른방식으로 처리된다. (윈도우의 크기를 바꾸는 것과 같은) 어떤 신호들은 간단히 무시된다; (잘못된 메모리 접근 같은) 다른 것들은 그 프로그램을 종료하여 처리된다. 단일쓰레드 프로그램들에서 signals을 처리하는 것은 간단하다: signals은 항상 한 프로세스에게 전달된다. 그러나, signals을 전달하는 것은 멀티쓰레드 프로그램에서 복잡하다. 그 프로그램에서 한 프로세스는 몇 가지 쓰레드들을 가질지도 모른다. 그러면 한 signal이 어디에 보내져야 하는가? 일반적으로 다음의 옵션들이 존재한다:
- 그 signal을 적용할 thread에 signal을 전달
- 그 프로세스에 있는 모든 thread에 signal을 전달
- 그 프로세스에 있는 어떤 threads들에 signal을전달
- 그 프로세스에 대해 모든 signals을 받는 특정한 thread를 할당.
- 한 signal을 전달하는 방법은 생성된 신호의 종류에 달려있다. 예를들어, synchronous signals는 그 프로세스에서 다른 쓰레드들이 아닌 그 signal을 발생시킨 thread에 전달될 필요가 있다. 그러나, asynchronous signals은 명백하지 않다. 어떤 asynchronous signals - 한 프로세스를 종료시키는 (control c같은) - 은 모든 쓰레드들에 보내져야 한다.
- 표준 UNIX는 signal을전달하는 함수가 kill과 pthread_kill 함수가 있다. kill은 특정 신호를 받을 process를 명시한다. UNIX의 대부분의 멀티쓰레딩 버전은 한 스레드가 어떤 신호를 받고 막을지를 명시하게 한다. 그래서 비동기 신호는 그 신호를 막지 않는 쓰레드들에게만 전달될 것이다. pthread_kill은 명시된 thread에게 신호를 보내도록한다. Windows는 explicitly하게 signals에 대한 support를제공하지 않지만, 그것은 asynchronous procedure calls (APCs)를 사용하여 그것들을 모방할 수 있게 한다. APC 기능은 한 user thread가 특정 사건의 알림을 받았을 때 호출될 한 함수를 명시할 수 있게 한다. 그것의 이름이 가리키듯이, APC는 UNIX에서 asynchronous signal과 같은 것이다. 그러나 UNIX는 멀티쓰레딩 환경에서 신호를 어떻게 다룰지를 극복하려는 반면에, APC facility는 좀 더 간단하다. 왜냐하면 APC는 프로세스 보다 특정 thread에 전해지기 때문이다.
- Thread cancellation은 그것이 완료되기 전에 한 쓰레드를 종료하는 것을 포함한다. 예를들어, 만약 여러 쓰레드들이 concurrently하게 한 database를 탐색하고, 한 thread가 그 결과를 반환한다면, 그 나머지 threads들이 종료될지도 모른다. 또 다른 상황은 한 사용자가 웹 페이지를 더 이상 불러오는 것을 막는 웹브라우저에 있는 한 버튼을 누를 때 발생한다. 종종 한 웹 페이지는 여러 쓰레드들을 사용하여 불러온다 - 각 이미지는 별개의 쓰레드에서 불러와진다. 한 사용자가 그 stop button을 누를 때, 그 페이지를 불러오는 모든 쓰레드들은 취소된다.
- 취소될 한 thread는 종종 target thread라고 언급된다. 한 target thread의 취소는 두 가지 다른 시나리오에서 발생할지도 모른다:
- 1. Asynchornous cancellation : 한 thread가 target thread를 즉시 종료한다.
- 2. Deferred cancellation : target thread는 주기적으로 그것이 종료해야 하는지를 체크한다. 그리고 그것에게 순서가 있는 방식으로 그것 자체를 종료할 기회를 준다.
- cacellation의 얼움은 자원이 a canceled thread에 할당되거나, 한 thread가 그것이 다른 쓰레드들과 공유하는 데이터를 업데이트하고 있는 중간에 취소되는 상황에서 발생한다. 이것은 특히 asynchronous cancellation과 문제가 된다. 종종, OS는 취소된 쓰레드로부터 시스템 자원을 찾아올 것이지만, 모든 자원들을 가져오지 못할 것이다. 그러므로, 한 thread를 비동기적으로 취소하는 것은 필요한 system-wide 자원을 해제하지 못할지도 모른다. 대조적으로, deferred cancellation으로, 한 쓰레드는 한 target thread가 취소될 것이라고 가리키지만, 취소는 그 타겟 쓰레드가 그것이 취소되어야 하는지를 결정하는 한 플래그를 확인하고 나서만 발생한다. 그 thread는 그것이 안전하게 취소될 수 있는 지점에서 이 check를 수행할 수 있다. Pthread에서, default cancellation type은 deferred cancellation이다. 여기에서 취소는 한 쓰레드가 cancellation point에 도달할 때에만 발생한다. 한 cancellation point를 만드는 한 기법은 pthread_testcancel() 함수를 불러오는 것이다. 취소 요청이 걸려있다고 발견된다면, cleanup handler라는 한 함수가 불러와진다. 이 함수는 한 쓰레드가 획득한 어떤 자원들이 그 쓰레드가 종료되기 전에 해제되도록 한다.
- 한 프로세스에 속한 쓰레드들은 프로세스의 데이터를 공유한다. 사실, 이 데이터 공유는 multithreaded programming의 장점 중의 하나를 제공한다. 그러나, 어떤 상황에서 각 thread는 어떤 데이터의 자신만의 복사본을 필요할지도 모른다. 우리는그러한 데이터를 thread-local storage (or TLS)라고 부를 것이다. TLS와 local variables을 혼동하는 것은 쉽다. local variables은 single function invocation 동안만 보일 수 있다. 반면에 TLS data는 function invocations을 넘어서 보인다. 어떤 면에서, TLS는 static data와 비슷하다. 그 차이는 TLS data는 각 쓰레드에 고유하다. Windows와 Pthreads를 포함한 대부분의 thread libraries는 thread-local storage에 대한 지원 형태를 제공한다. 자바도.
- 멀티쓰레디드 프로그램과 고려해야 될 마지막 문제는 kernel과 thread library 사이의 통신과 관련이 있다. 그 통신은 many-to-many와 two-level models에 의해 요구될지도 모른다. 그러한 조정은 최고의 성능을 보장하는 것을 돕기위해 kernel threads들의 개수가 동적으로 조정될 수 있게 한다. many-to-many or two-level model 둘 중 하나를 구현하는 많은 시스템들은 user와 kernel threads 사이에 중간 자료구조를 둔다. 일반적으로 lightweight process 또는 LWP라고 알려져 있다. user-thread library에게, LWP는 virtual processor인 것 처럼 보이는데, 거기에서 그 어플리케이션은 작동할 user thread를 스케쥴링 할 수 있다. 각 LWP는 kernel thread에 부착되고, OS는 물리적 프로세서에 작동할 threads들을 스케쥴링한다. 만약 한 kernel thread가 봉쇄된다면 (I/O 연산이 완료되기를 기다리는 것 같은), 그 LWP 또한 봉쇄된다. 그 chain위로, 그 LWP에 부착된 user-level thread 또한 봉쇄된다.
- user-thread library와 kernel 사이의 통신에 대한한 전략은 scheduler activation으로 알려져있다. 그것은 다음과 같이 작동한다: kernel은 한 어플리케이션에 virtual processors의 한 집합 (LWPs)를 제공하고, 그 어플리케이션은 user threads를 스케쥴링 하여 available virtual processor에 넣는다. 게다가, 그 kernel은 한 어플리케이션에게 어떤 사건들에 대해 알려야 한다. 이 절차는 upcall이라고 알려져있다. Upcalls은 upcall handler로 thread library에 의해 처리된다. upcall handlers는 virtual process에서 작동해야만 한다. 한 upcall을 촉발시키는 한 event는 한 application thread가 block되려고 할 때 발생한다. 이 시나리오에서, 그 kernel은 어플리케이션에게 upcall을 만드는데, 그것에게 한 쓰레드가 block되려고 한다고 알리고 특정 thread를 명시한다. 그 kernel은 그러고나서 새로운 virtual processor를 그 어플리케이션에게 할당한다. 그 어플리케이션은 이 새로운 virtual processor에서 upcall handler를 작동시킨다. 그 프로세서는 blocking thread의 상태를 저장하고, 그 blocking thread가 작동하고 있는 virtual process를 포기한다. 그 upcall handler는 그러고나서 새로운 virtual processor에 작동할 자격이 있는 또 다른 쓰레드를 스케쥴링 한다. 그 봉쇄되고 있는thread가 그 이벤트가 발생하기를 기다릴 때, 그 kernel은 thread library에 또 다른 upcall을 만드는데, 그것에게 이전에 blocked thread가 이제 작동할 자격이 있다고 알린다. 이 사건에 대한 upcall handler는 ㄸ한 virtual processor를 요구하고, 그 kernel은 새로운 virtual processor를 할당하고, 그 사용자 쓰레드들 중 하나를 제외하고, 그 upcall handler를 그것의 virtual processor에 작동시킨다. unblocked thread를 실행할 자격이 있다고 마킹한 후에, 그 어플리케이션은 이용가능한 virtual processor에 작동할 수있는 자격이 있는 쓰레드를 스케쥴링 한다.
* Operating-System Examples
- 윈도우즈 어플리케이션은 별개의 프로세스에서 작동하고, 각 프로세스는 한 개 이상의 쓰레드들을 가질지도 모른다. 부가적으로 Windows는 one-to-one mapping을사용하고, 거기에서 각 user-level thread는 관련된 kernel thread에 매핑된다. 한 thread의 일반적인 컴포넌트들은 다음을 포함한다:
- 그 thread를 유일하게 확인하는 thread ID
- processor의 상태를 나타내는 register set
- 그 thread가 user mode에서 작동할 때 이용되는 user stack과, 그 thread가 kernel mode에서 작도할 때 이용되는 kernel stack.
- 다양한 run-time libraries와 dynamic link libraries(DLLs)에 의해 사용된 private storage area.
- register set, stacks, private storage area는 그 thread의 context로 알려져있다. 한 thread의 주된 자료구조는 다음을 포함한다. ETHREAD : 실행하는 thread block. KTHREAD : kernel thread block. TEB - tread environment block. ETHREAD의 주된 컴포넌트들은 그 쓰레드가 속한 프로세스에 대한 포인터와 그 쓰레드가 제어를 시작하는 routine의 주소를 포함한다. ETHREAD는 또한 대응되는 KTHREAD에 대한 포인터를 포함한다. KTHREAD는 그 thread에 대한 scheduling과 synchronization information을 포함한다. 게다가, KTHREAD는 kernel stack (그 thread가 kernel mode에서 작동할 때 사용되는)과 TEB에 대한 포인터를 포함한다. ETHREAD와 KTHREAD는 전적으로 kernel space에서 존재한다; 즉, 이것은 kernel만이 그것들에 접근할 수 있다는 것을 의미한다. 그 TEB는 그 thread가 user mode에서 작동할 때 접근되는 user-space data structure이다. 다른 필드 사이에서, 그 TEB는 thread identifier, user-mode stack, thread-local storage에 대한 array를 포함한다.
- Linux는 프로세스를 복제하는 전통적인 기능으로 fork() system call을 제공한다. Linux는 또한 clone() system call을 사용하여 threads들을생성하는 기능을제공한다. 그러나, Linux는 processes와 threads사이를 구분하지 않는다. 사실, Linux는 task라는 용어를 사용한다 - process or thread보다 - 한 프로그램 내에서 제어의 흐름을 언급할 때. clone() 불러와질 때, 그것은 parent와 child tasks 사이에 얼마나 많은 공유가 발생해야 하는지를 결정하는 flags들의 한 집합을 넘겨 받는다. sharing의 다양한 수준이 가능한데, task가 Linux kernel에서 표현되는 방식 때문이다. 고유 kernel 자료구조가 그 시스템에서 각 task에 대해 존재한다. 그 task에 대한 데이터를 저장하는 대신에, 이 자료구조는 이러한 데이터가 저장되는 다른 자료구조에 대한 포인터를 포함한다. 예를들어, open files, signal-handling information, virtual memory의 list를 나타내는 자료구조들. fork()가 불러와질 때, 새로운 task가생성되고, 부모 프로세스의 모든 관련된 자료구조의 복사와 함께. 새로운 task는 또한 clone() system call이 만들어질 때 생성된다. 그러나, 모든 자료구조들을 복사하는 대신에,그 새로운 tasks는 clone() 넘겨진 flags의 집합에 따라 그 부모 task의 자료구조들을 가리킨다.
댓글 없음:
댓글 쓰기