CHAPTER 4 - 스레드(Threads)

CH 4에서는,
- 첫째로, CPU 이용의 기본 단위인 Threads에 대해서 알 수 있다.
- 둘째로, Pthreads API로 대표되는 Thread Library에 대해서 알 수 있다.
- 셋째로, Multi-Threaded Programming에 관련된 issue에 대해서 알 수 있다.


4.1 개요 (Overview)

- Thread는 CPU 이용의 기본 단위이다.
- Thread는 Thread ID, Program Counter, Register set, stack으로 구성된다.
- Thread는 같은 프로세스에 속한 다른 Thread와 Code, Data Section 등을 공유한다.
- Single Threaded process와 Multi-threaded process가 있다.

Single-threaded processes(좌) and multi-threaded processes(우)

 

* 동기(Motivation)

- 현대의 컴퓨터에서 동작하는 거의 모든 Software application들은 multi-threaded이다.

- 한 Application이 여러 개의 similar 한 task를 처리해야 할 때도 존재한다.
- 예를 들어, Web server는 client로부터 web page, image 등에 대한 요청을 받는다.
- 만일 Web server가 Single-threaded하다면, 한 번에 하나의 클라이언트만 서비스할 수 있게 되어
  클라이언트는 자신의 요구가 서비스되기까지 매우 긴 시간을 기다려야 한다.

- 한 해결책은 서버에게 서비스 요청이 들어오면, 프로세스는 그 요청을 수행할 별도의 프로세스를 생성하는 방법이다.
- 하지만, 프로세스 생성 작업은 매우 많은 시간과 자원을 소비해야 하기 때문에 오버헤드가 많아지게 된다.
- 따라서, 이보다는 프로세스 안에서 여러 Thread를 만들어 나가는 것이 더 효율적이다.

- Thread는 RPC(Remote Procedure Call)에서도 매우 중요한 역할을 담당한다.
- RPC는 IPC를 마치 일반적인 함수나 procedure call을 하는 것처럼 할 수 있게 한다.
- RPC Server들은 대부분 Multi-Threaded System이다.

- 많은 운영체제 kernel들은 현재 multi-threaded의 형태를 갖추고 있다.
- kernel 안에서 다수의 Thread들이 동작하고, 각 thread는 인터럽트 처리 등 특정 작업을 수행한다.

* 장점(Benefits)

- Multi-threaded programming의 benefit은 크게 4가지 부류로 나눌 수 있다.

1. 응답성(Responsiveness)
- interactive application을 Multithreading 하면, 일부분이 blocking 되거나 긴 작업을 수행하더라도  
  프로그램의 수행이 계속되도록 하여, user에 대한 Responsiveness를 향상시킨다.
- 이 특성은 user interface를 설계하는데 유용하다.

2. 자원 공유(Resource sharing)
- 프로세스는 공유 메모리(Shared Memory)와 메시지 전달(Message Passing) 기법을 통하여 자원을 공유할 수 있다.
- Thread는 자동적으로 그들이 속한 프로세스의 resource 들과 memory를 공유한다.
- Code와 Data 공유의 이점은 한 application 이 같은 address space 내에 여러 개의 다른 작업을 하는 Thread를 가질 수 있다는 점이다.

3. 경제성(Economy)
- 프로세스 생성을 위해 memory와 resource를 할당하는 것은 비용이 많이 든다.
- Thread는 자신이 속한 process의 자원들을 공유하기에 Thread를 생성하여 Context-swtich를 하는 것이 경제적이다.
- 일반적으로 Thread를 사용하는 것보다 새로운 process를 생성하고 관리하는 것이 훨씬 더 많은 시간을 소모한다.

4. 규모 적응성(Scalability)
- Multithreading의 이점은 multi-processor 구조에서 더욱 증가할 수 있다.
- 해당 구조에서는 각각의 Thread가 다른 processor에서 parallel 하게 수행될 수 있기 때문이다.

 


4.2 다중 코어 프로그래밍(Multicore Programming)

- core가 여러 CPU chip 형태를 띠거나, 칩 안에 여러 개가 존재하게 될 때,
  이러한 시스템을 다중 코어(multicore) 또는 다중 처리기(Multiprocessor) 시스템이라고 부른다.

- Single computing core 상에서, concurrency는 단순히 Thread의 실행이 시간에 따라 교대로 실행된다는 것을 의미한다.

- Multiple core System 상에서는 시스템이 개별 Thread를 각 core에 배정할 수 있기 때문에 concurrency는 Thread들이 parallel 하게 실행될 수 있다는 것을 의미한다.

- 하나 이상의 task를 동시에 수행할 수 있는 시스템에 대해 병렬적(parallel)이라고 말한다.

Parallel execution(병렬 실행) on a multicore system

 

- 병행(Concurrent) 실행 시스템은 모든 task가 진행하게끔 함으로써 하나 이상의 task를 지원한다.

Concurrent execution(병행 실행) on a single-core system

- 따라서 병렬(Parallel) 실행 없이 병행(Concurrent) 실행하는 것이 가능하다.
- CPU 스케줄러는 시스템의 프로세스 사이를 빠르게 오고 가며 모든 프로세스를 진행시켜 마치 병렬 실행하는 듯한 착각을 주게 한다.
  (실제는 병행 실행을 수행하는 것)

 

* 프로그래밍 도전과제(Programming Challenges)

- Multi-core System을 위해 Programming 하기 위해서는 다섯 개의 주의점이 있다.

(1) 태스크 인식(Identifying tasks)
- Application을 분석하여 separate, concurrent task로 나눌 수 있는 영역을 찾는 작업이 필요하다.

(2) 균형(Balance)
- 찾아진 부분들이 전체 작업에 균등한 기여도를 가지도록 task를 나누는 것도 중요하다.

(3) 데이터 분리(Data spliting)
- Application이 여러 task로 나뉘는 것처럼, task가 접근하는 data 또한 각 core에서 사용할 수 있도록 나누어져야 한다.

(4) 데이터 종속성(Data dependency)
- task가 접근하는 데이터는 둘 이상의 task 사이에 dependency가 없는지 검토되어야 한다.

(5) 시험 및 디버깅(Testing and debugging)
- program이 multi-core에서 parallel 하게 실행될 때, 디버깅의 어려움이 높아진다.

 

* 병렬 실행의 유형 (Type of Parallelism)

1. 데이터 병렬 실행(Data Parallelism)은 동일한 data의 부분집합을 다수의 계산 코어에 분배한 뒤,
각 코어에서 동일한 연산을 수행하는 데 초점을 둔다. (하나의 일을 여러 core가 함께 수행함.)

2. 태스크 병렬 실행(Task Parallelism)은 data가 아니라 task(Thread)를 다수의 코어에 분배한다.
각 thread는 고유의 연산을 수행하므로, 동일한 데이터에 대해 연산을 수행할 수도 있다.

- 즉, data parallelism은 data가 core에 분배되어야 하고, task parallelism은 task가 분배되어야 한다.
- 보통 이 두 가지 유형(Data Parallelism, Task Parallelism)을 혼합하여 사용한다.

 

 


4.3 다중 스레드 모델(Multithreading Models)

- Thread는 사용자 수준에서는 user Thread와 kernel 수준에서 사용되는 kernel thread로 제공될 수 있다.
- User Thread는 Kernel 위에서 지원되며, kernel의 지원 없이 관리된다.
- Kernel Thread는 운영체제에 의해 직접 지원되고 관리된다.

* 다대일 모델(Many-to-One Model)

- 많은 user-level Threads들을 하나의 kernel thread로 mapping 한다.

(장점)
- Thread management는 user space의 thread library에 의해 행해지고, 효율적이다.

(단점)
- 하지만, 한 thread가 blocking system call을 하면 전체 process가 block 된다.
- 또한, 한 번에 한 thread만이 kernel에 접근할 수 있기 때문에 multiple thread가
  multi-core system에서 병렬적(parallel)으로 실행될 수 없다.

many-to-one model

 

* 일대일 모델(One-to-One Model)

- 각 User Thread를 Kernel thread로 mapping 한다.

(장점)
- 이 모델은 하나의 Thread가 blocking system call을 하더라도, 다른 Thread가 실행될 수 있기 때문에
다대일(Many-to-One Model) 모델보다 더 많은 병렬성(parallel)을 제공한다.
- 이 모델은 Multiprocessor에서 multiple thread가 병렬로 수행될 수 있다.

(단점)
- 이 모델의 단점은, user-level thread를 생성할 때 그에 따른 kernel thread를 생성해야 한다는 점이다.
- Kernel thread를 생성하는 overhead가 Application의 성능을 저하시킬 수 있으므로, 이 모델의 대부분의 구현은 system에 의해 지원되는 Thread의 수를 제한한다.

one-to-one model

 

* 다대다 모델(Many-to-Many Model)

- 여러 개의 User-level thread를 그보다 작은 수 혹은, 같은 수의 kernel thread로 multiplex 한다.
- kernel thread의 수는 Application이나 특정 기계에 따라 결정된다.

many-to-many model

 

* 두 수준 모델(Two-level Model)

- (Many-to-Many model)과 (One-to-One Model)을 결합한 모델이라고 할 수 있다.

two-level model

 


4.4 스레드 라이브러리(Thread Library)

- Thread Library는 프로그래머에게 Thread를 생성하기 관리하기 위한 API를 제공한다.

- Thread Library를 구현하는 데에는 크게 2가지 방법이 있다.
(1) kernel의 지원 없이 완전히 사용자 공간에서만 라이브러리를 제공하는 방법
(2) OS에 의해 지원되는 kernel 수준에서의 library를 구현하는 방법

- 세 종류의 Thread Library가 주로 사용된다.
(1) POSIX Pthreads (2) Windows (3) Java

- 쓰레딩(Threading)에는 2가지 종류가 있다.

(1) 동기 쓰레딩(Synchronous threading)
- 부모 thread가 하나 이상의 자식 thread를 생성하고 자식 thread가 모두 종료할 때까지 기다렸다가 자신의
실행을 재개하는 방식을 말한다. 이 방식은 '포크-조인(fork-join)' 전략이라고 불린다.
- 부모가 생성한 Thread들은 병행하게(concurrently) 실행되지만 부모는 자식들의 작업이 끝날 때까지
실행을 계속할 수 없다.
- 동기 쓰레딩(Synchronous threading)은 상당한 양의 데이터를 공유하게 된다.
- 예를 들어, 부모 thread는 자식들이 계산한 결과를 통합할 수 있어야 한다.

(2) 비동기 쓰레딩(Asynchronous threading)
- 부모 thread가 자식 thread를 생성한 후, 부모는 자신의 실행을 재개하여 두 thread를 병행하게 실행한다.
- 각 Thread는 모든 다른 Thread와 독립적으로 실행하기 때문에, 부모 thread는 자식의 종료를 알 필요가 없다.
- 각 Thread들은 독립적이기 때문에, thread들 사이의 데이터 공유는 거의 없다.

 


4.5 암묵적 스레딩(Implicit Threading)

- Multi-core processing이 발전함에 따라, 수백 또는 수천 개의 Thread를 가진 Application이 등장하였다.

- Threading의 생성과 관리 책임을 Application developer로부터 compiler와 run-time libraries에게 넘겨주는 '암묵적 스레딩(implicit threading)'을 사용하여 multi-thread application를 효율적으로 설계할 수 있다.

- 암묵적 스레딩(implicit threading)을 사용하여 multicore processor를 활용할 수 있는 multi-thread program을 설계하는 3가지 접근법은 다음과 같다. (1) Thread Pool (2) OpenMP (3) Grand Central Dispatch

* 스레드 풀(Thread Pool)

웹 서버는 요청을 받을 때마다 그 요청을 들어주기 위해 새로운 Thread를 생성한다.
- 이 방법에는 크게 2가지 문제가 있을 수 있다.
  (1) 서비스를 할 때마다 Thread를 생성하는 데 일정 시간이 소요된다.
  (2) 모든 요청마다 Thread를 생성하므로, 동시에 실행할 수 있는 최대 Thread의 수를 정해놓아야 한다.

- Thread를 무한정 만들면 언젠가는 CPU 시간, 메모리 공간과 같은 System Resource가 고갈될 수 있다.
- 이러한 문제들을 해결해줄 수 있는 방법 중 하나가 '스레드 풀(Thread pool)'이다.

- Thread pool의 기본 아이디어는 프로세스를 시작할 때 아예 일정한 수의 Thread를 미리 pool로 만들어 놓는 것이다.
- pool에 만들어진 Thread들은 일(work)이 없을 때는, 일(work)을 계속 기다리게 된다.
- 이후 request가 들어오게 되면, pool에서 한 Thread에게 해당 request를 할당한다.
- 만약 pool에 남아 있는 Thread가 없다면, server는 free thread가 하나 생길 때까지 기다려야 한다.

- Thread Pool은 아래와 같이 3가지의 장점을 가진다.
(1) 새 Thread를 만들어 주는 것보다 생성된 Thread를 사용하여 서비스해 주는 것이 더 빠르다.
(2) Thread pool은 임의의 시간에 존재할 수 있는 Thread 개수에 제한을 둔다.
(3) task를 생성하는 방법을 분리하여, task를 일정 시간 후에 실행되도록 하거나, 주기적으로 실행시킬 수 있다.

- Thread pool에 존재하는 Thread의 개수는 CPU 수, Physical Memory 용량 등을 고려하여 정해질 수 있다.

 


4.6 스레드와 관련된 문제들(Threading Issues)

 

* Fork() 및 Exec() System call

- Multithreaded Program에서는 fork()와 exec()의 의미가 달라질 수 있다.

Q) 만일, 한 프로그램의 Thread가 fork()를 호출하면, 새로운 프로세스는 모든 Thread를 복제해야 하는가?
한 개의 Thread만 가지는 프로그램이어야 하는가?

A) 몇몇 UNIX는 이 두 가지 버전의 fork()를 모두 지원한다.

 

* 신호처리(Signal Handling)

- Signal은 UNIX에서 프로세스에게 어떤 사건이 일어났음을 알려주기 위해 사용된다.
- Signal은 알려줄 event의 source나 reason에 따라 동기식 또는 비동기식으로 전달될 수 있다.

- 모든 신호는 다음과 같은 형태로 전달된다.
1. 신호는 특정 사건이 일어나야 생성된다.
2. 생성된 신호가 프로세스에게 전달된다.
3. 신호가 전달되면 반드시 처리되어야 한다.

- 동기식 신호(Synchronous signal)의 예로는 불법적인 메모리 접근, 0으로 나누기 등이 있다.
- 비동기식 신호(Asynchronous signal)의 예로는. (Ctrl+C)와 같은 키를 눌렀을 때가 해당된다.

- 모든 신호는 둘 중 하나의 handler에게 처리된다.
   1. 디폴트 신호 처리기(Default signal handler)
   2. 사용자 정의 신호 처리기(A user-defined signal handler)
- deafult singal handlers는 user-defined signal handler에 의해 대체될 수 있다.

- 동기적 신호는 해당 신호를 발생시킨 Thread에게 전달되어야 하고, 다른 Thread에게는 전달되면 안 된다.
- 비동기적 신호는 누가 신호를 발생시켰는지 명확하지 않으므로, 모든 Thread에게 신호가 전달된다.

* 취소 (Cancellation)

- 스레드 취소(Thread Cancellation)은 Thread가 끝나기 전에 그것을 강제 종료시키는 작업을 말한다.
- 예를 들어, 여러 Thread들이 데이터베이스를 병렬로 검색하고 있다가 한 Thread가 원하는 결과를 찾게 되면 나머지 Thread들은 취소되어도 된다.
- 이처럼 취소되어할 Thread들을 목적 스레드(Target Thread)라고 부른다.

- 취소(Cancellation)은 아래 두 가지와 같은 방식으로 발생할 수 있다.
(1) 비동기식 취소(Asynchronous cancellation) : 한 Thread가 즉시 Target Thread를 강제 종료시킨다.
(2) 지연 취소(Deferred cancellation) : Target Thread가 주기적으로 자신이 강제 종료되어야 할지 점검한다.

- Thread 취소를 어렵게 만드는 것은 취소 Thread에게 할당된 Resource의 문제이다.
- 또한, Thread가 다른 Thread와 공유하는 자료구조를 갱신하는 과정에서 취소 요청이 와도 문제가 될 수 있다.
- 이에, 비동기식으로 Thread를 취소하면 필요한 시스템 자원을 다 사용 가능한 상태로 만들지 못할 수도 있다.
- 지연 취소의 경우, Thread는 자신이 취소되어도 안전하다고 판단될 때 취소 여부를 검사할 수 있다.

- Cancellation의 Default 유형은 지연 취소(Deferred Cancellation)이다.

* 스레드 국지 저장소(Thread-Local Storage)

- 한 프로세스에 속한 Thread들은 그 프로세스의 data를 모두 공유한다.
- 그러나, 상황에 따라 각 Thread는 자기만 Access 할 수 있는 데이터를 가져야 할 필요가 있을 수 있다.
- 이러한 data를 스레드 국지 저장소(Thread-Local Storage, TLS)라고 부른다.

- TLS를 local variable과 혼동하기 쉽다.
- Local Variable이 하나의 함수가 호출되는 동안에만 보이는 반면에 TLS는 전체 함수 호출에 걸쳐 보인다.
- TLS는 static data와 유사한데, 차이점은 TLS data는 각 Thread의 고유한 정보를 저장한다는 점이다.

* 스케줄러 액티베이션(Scheduler Activation)

- Multithread Program에서는 Thread Library와 kernel의 통신 역시 고려해야 한다.
- 이 통신은 Many-to-Many, Two-level Model에서 반드시 해결해야 하는 문제이다.
- 이러한 통신을 통해 Kernel Thread의 수를 동적으로 조절하여 Application이 최고의 성능을 보이도록 보장할 수 있다.

- Many-to-Many 또는 Two-level model을 구현하는 많은 시스템들은 user와 kenrel thread 사이에  
  중간 자료 구조를 둔다. 이 자료구조는 보통 lightweight process, or LWP라고 불린다.
- LWP는 하나의 kenrel thread에 속해있으며, kernel thread가 block 되면 LWP 역시 함께 block 된다.

lightweight process(LWP)

 

- user thread library와 kernel thread 간의 통신 방법 중의 하나는 Scheduler Activation이다.

- Scheduler Activation은 아래와 같이 동작한다.
(1) kernel은 application에게 virtual processor(LWP)의 set을 제공한다.
(2) Application은 virtual processor로 user thread를 schedule 한다.
(3) kernel은 application에게 certain event에 대해 알려줘야 한다. (이를 'upcall'이라고 한다.)

 


4.7 운영체제 사례(Operating-System Examples)

* Windows 스레드 (Windows Threads)

- Thread를 위해 아래와 같은 자료구조를 가지고 있다.
(1) ETHREAD : 실행 스레드 블록(Executive Thread block):
    Thread가 속한 프로세스를 가리키는 포인터, Thread가 실행을 시작해야 할 루틴의 주소 등을 가짐.

(2) KTHREAD : 커널 스레드 블록(Kernel Thread Block):
    Thread의 스케줄링 및 동기화 정보, kernel mode에서 실행될 때 사용되는 kernel stack 등을 가짐.

(3) TEB : 스레드 환경 블록(Thread Environment Block):
    Thread identifier, user-mode stack, thread-local storage를 위한 array 등을 가짐.

 

* Linux 스레드 (Linux Threads)

- 프로세스를 복제하는 fork() system call을 제공하며, clone() system call을 통해 Thread를 생성할 수 있다.
- Linux에서는 프로세스(process)와 스레드(Thread)를 구분하지 않으려고 한다.


-Reference-
Abraham Silberschatz, Peter B. Galvin, Greg Gagne의 『Operating System Concept 9th』