쓰레드

카테고리 없음

2019. 4. 15. 22:28

쓰레드 ( Thread )

쓰레드는 CPU Utilization의 근본 단위이다. 쓰레드는 프로세스와 달리 데이터와 코드 영역, 파일 및 I/O 자원을 공유한다.  따라서 새 프로세스가 할 일이 기존 프로세스가 하는 일과 동일하다면 굳이 프로세스를 생성하지 않고 쓰레드를 사용하는게 효율적일 것이다. 대부분의 운영체제 커널은 다수의 쓰레드로 다중화되어 장치 또는 인터럽트 처리 등의 특정 작업을 수행한다.

 

쓰레드를 사용하는 것에는 다음과 같은 장점들이 있다.

 

- 응답성 ( Responsiveness ) : 만약 시간이 적게 걸리는 일과 많이 걸리는 일이 동시에 수행되어야 하는 경우 쓰레드를 사용하지 않는다면 시간이 많이 걸리는 일이 수행되는 동안 다른 일을 수행할 수 없겠지만, 다중 쓰레드를 사용하여 시간이 많이 걸리는 일을 할당한다면 동시에 두 가지의 일에 대한 응답이 가능할 것이다.

- 자원 공유 ( Resource Sharing ) : Shared Memory 또는 Message Passing 방식으로 자원을 공유해야 하는 프로세스와는 달리, 쓰레드는 그들이 속한 코드와 데이터 영역을 공유하므로 같은 작업을 수행하는 경우 오버헤드 없이 이득을 볼 수 있다.

- 경제성 ( Economy ) : 프로세스를 생성하는 것 보다 쓰레드를 생성하고 Context Switching하는것이 훨씬 경제적이다.

- 규모 적응성 ( Scalability ) : 멀티 프로세서 환경에서는 태스크가 병렬로 수행되어 단일 프로세스 환경에서보다 더 빨리 태스크를 수행할 수 있다.

 

멀티코어 프로그래밍 ( Multicore Programming )

단일프로세서 환경에서 여러개의 스레드가 존재한다면 이 스레드들은 시분할 방식으로 동작할 것이다. 즉, 특정 시간에 하나의 스레드만 처리될 것이다. 반면에 멀티코어 환경에서는 여러개의 스레드가 각각의 코어에 할당되어 스레드들이 병렬적으로 수행될 수 있다. 특정 시간에 두 개 이상의 스레드가 수행될 수 있는것이다.

전자처럼 여러 개의 스레드가 시분할 방식으로 동시에 수행되는것 처럼 착각을 불러일으키는 것이 병행성(Concurrency)이고, 후자처럼 여러 개의 스레드가 실제로 동시에 수행되는 것이 병렬성(Parallelism)이다.

 

이러한 병렬성 ( Parallelism )은 두 가지의 종류가 있다.

- Data Parallelism : 동일한 데이터를 다중 코어에 분배하여 처리하는 것

- Task Parallelism : 동일한 태스크를 다중 코어에 분배하여 처리하는 것

현실적으로는 각각의 병렬성이 따로 구현된다기 보다는 혼용하여 사용하는 경우가 많다.

 

다중 쓰레드 모델 ( Multithreading Models )

쓰레드를 더 구체화 해보자면, 사용자 수준의 유저 쓰레드커널 수준의 커널 쓰레드가 존재한다. 유저 쓰레드와 커널 쓰레드 사이에서는 어떠한 연관관계가 존재하며, 다음의 세 가지가 있다.

 

- 다대일 모델 ( Many-to-One Model ) : 하나의 커널 쓰레드에 여러 개의 유저 쓰레드가 사상됨

- 일대일 모델 ( One-to-One Model ) : 하나의 커널 쓰레드에 단 하나의 유저 쓰레드가 사상

- 다대다 모델 ( Many-to-Many Model ) : 여러 개의 커널 쓰레드에 여러 개의 유저 쓰레드가 사상됨

 

다대일 모델은 멀티코어 환경에서는 효용성이 없고, 일대일 모델이 가장 많이 사용된다. 일대일 모델은 멀티코어 환경에서의 병렬성을 보장하지만, 사용자 쓰레드를 생성할 때 커널 쓰레드를 동시에 생성해야 한다는 오버헤드가 있다.

 

쓰레드 라이브러리 ( Thread Library )

쓰레드 라이브러리는 프로그래머가 쓰레드를 생성하고 관리할 수 있도록 API를 제공한다. 이 API는 유저 수준의 쓰레드의 생성과 관리만을 제공할 수도 있고, 커널 수준의 쓰레드의 생성과 관리를 제공할수도 있다. 또는 둘을 동시에 지원할 수도 있다.

POSIX 표준의 Pthreads는 유저와 커널 수준의 라이브러리이고, Windows Thread Library는 커널 수준의 라이브러리, 그리고 Java Thread API는 호스트 운영체제에 따라서 달라진다.

 

스레드의 생성과 관련하여, 비동기 쓰레딩과 동기 쓰레딩의 두 가지 전략이 있다.

 

- Asynchronous Threading : 부모가 자식 스레드를 생성한 후 부모와 자식 스레드가 병렬적으로 수행되는 전략

- Synchronous Threading : 부모가 자식 스레드를 생성한 후 부모가 자식 스레드가 종료될 때까지 기다렸다가, 종료되면 다시 수행되는 전략. 이는 Fork-Join방식으로도 불린다. Fork-Join 방식에서 부모는 자신이 생성한 한 개 이상의 쓰레드들에 대하여 그 자식 쓰레드들이 모두 종료될 때까지 실행을 재개할 수 없다.

 

암묵적 쓰레딩 ( Implicit Threading )

멀티코어 시스템이 지속적으로 성장하면서 프로그래머는 수백개 또는 수천개 이상의 쓰레드를 관리해야 하는 어려움에 처했다. 이러한 어려움들을 컴파일러와 런타임 라이브러리에게 넘겨줄 수 있게 하는 기법이 Implicit Threading이다. 암묵적 쓰레딩에는 크게 세 가지의 방법이 있다.

 

- Thread Pool : 프로세스가 시작할 때 일정한 수의 쓰레드들을 미리 만들어 둔다. 작업요청이 들어오면 쓰레드를 할당하여 작업을 처리하도록 하고, 작업이 끝나면 다시 pool로 돌아가 다음 작업을 기다리게 한다.

- OpenMP : C, C++, FORTRAN으로 작성된 API컴파일러 지시자의 집합이다. 프로그래머는 병렬로 실행시키고자 하는 블록을 찾아 컴파일러 지시자를 삽입하여 병렬 영역으로 선언함으로써, OpenMP 런타임 라이브러리에게 해당 병렬 영역을 멀티쓰레딩으로 처리하라고 지시한다. 이렇게 하게 되면 시스템의 코어 개수만큼 쓰레드가 생성된다.

- GCD ( Grand Central Dispatch ) : 병렬수행을 원하는 블록을 Dispatch Queue에 넣어서 병렬로 수행될 수 있도록 스케쥴링 하는 방식이다. 이 Dispatch Queue에 들어간 블록들은 직렬로 하나씩 꺼내져 처리될 수도 있고, 병렬로 여러개가 꺼내져 처리될 수도 있다.

 

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

멀티쓰레딩을 활용하는 프로그램을 설계할 때 고려해야 할 몇가지의 사안들이 있다.

 

- Fork()와 Exec() 시스템 콜

만약 어떤 프로세스에 존재하는 여러개의 쓰레드들 중 하나의 쓰레드가 fork()를 호출할 경우 해당 프로세스의 전체 쓰레드를 복제해야 하는가, 아니면 호출한 쓰레드만 복제해야 하는가 ?

이는 exec() 시스템 콜의 호출시점에 따라 달라질 수 있다. exec()은 모든 스레드를 포함한 현재 프로세스를 매개변수로 지정된 프로그램으로 대체해 버린다. 따라서 만약 fork() 후 곧바로 exec()을 호출할 것이라면, 전체 쓰레드를 복제할 필요가 없을것이다. 왜냐하면 어차피 전체 쓰레드가 다른 프로그램으로 대체될 것이기 때문이다. 하지만 fork() 한 스레드에서 특정 작업을 수행해야 한다면 전체 쓰레드들을 복제하는 편이 좋을수도 있다. 

 

- Signal Handling

신호(Signal)는 프로세스에게 어떠한 사건이 일어났음을 알려주기 위해 사용되는 것이다.

Synchronous Signal : divide by zero나 illigal memory access와 같이 자신에 의해 발생하는 신호

Asynchronous Signal : Ctrl + C와 같이 다른 프로세스로 부터 받는 신호

신호가 동기식이냐, 비동기식이냐를 떠나서 멀티쓰레드 환경에서는 어떤 쓰레드에게 신호를 전달해야 하는가?

동기식 신호인 경우 스스로 만들어낸 신호이므로 신호를 만들어 낸 쓰레드에게만 신호가 전달되어야 한다. 하지만 비동기식 신호인 경우에는 그 신호가 무엇인지에 따라 모든 쓰레드에게 전달될 수도 있고, 일부 쓰레드에게만 전달될 수도 있다.

UNIX에서는 프로세스에게 신호를 보낼 때 kill(), 프로세스 내의 특정 쓰레드에게 신호를 보낼 때는 pthread_kill()을 사용한다.

 

- Thread Cancellation

쓰레드가 끝나기 전에 취소하는 것을 Thread Cancellation이라고 한다. 취소하고자 하는 쓰레드를 Target Thread라고 하는데, 이 Target Thread는 두 가지 방식으로 취소될 수 있다.

Asynchronous Cancellation : 한 쓰레드가 즉시 Target Thread를 강제 종료시킨다.

Deferred Cancellation : Target Thread 자신이 강제 종료되어야 할지를 주기적으로 점검한다.

지연 취소의 경우 스레드에게 할당된 자원의 회수가 가능하지만, 비동기 취소의 경우 스레드에게 할당된 자원의 회수가 불완전할수도 있다.

 

- TLS ( Thread Local Storage )

하나의 프로세스에 속한 쓰레드들은 해당 프로세스의 코드와 데이터 자원을 공유하지만, 때로는 각 쓰레드마다 고유한 스토리지를 가져야 할 필요가 있는데, 이를 위한 것이 TLS이다.

 

- Scheduler Activation

하나의 코어에 여러개의 쓰레드가 할당될 수 있는경우 스케쥴링이 필요한데, 이는 User Thread와 Kernel Thread 사이에 존재하는 LWP(Light Weight Process)에 의해 활성화 된다.

 

참조

ABRAHAM SILBERSCHATZ, PETER BAER GALVIN, GREG GAGNE / 운영체제 / (주)교보문고 / 2018년