Skip to content

Multi Thread

1개의 응용 프로그램이 스레드(thread)로 불리는 처리 단위를 복수 생성하여 복수의 처리를 병행하는 것. 즉, 응용 프로그램 내에서의 다중 작업(multitasking) 처리를 말한다. 다중 작업과 같이 중앙 처리 장치(CPU)의 처리 시간을 매우 짧은 단위로 분할하여 복수의 스레드에 차례로 할당함으로써 복수의 처리가 동시에 이루어지는 것처럼 보인다.

혼동하기 쉬운 개념 중 하나로 프로세스(Process)와 스레드(Thread)의 차이점이 있다. 프로세스는 하나의 프로그램 단위 이며, 스레드는 프로세스 내부의 가장 작은 작업 단위이다.

스레드 생성시 핸들(Handle)과 아이디(ID)의 사용방법은 다음과 같다. 핸들은 동일 프로세스 내부로 유효범위가 정해져 있으며, 아이디는 다른 프로세스간 통신을 위하여 사용된다.

Thread

스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이러한 실행 방식을 멀티스레드(multithread)라고 한다.

Windows vs Linux

Windows와 Linux에서 사용하는 프로세스 및 스레드 구현 함수가 다르기 때문에 아래와 같이 정리한다.

기능

Windows

Linux

Process 생성

CreateProcess

fork
exec

Process 제거

TerminateProcess

kill

객체대기(동기화)

WaitForSingleObject

waitpid
pthread_join
pthread_detach

Thread 우선순위 변경, 확인

SetThreadPriority
GetThreadPriority

setpriority
getpriority

Process ID 확인

GetCurrentProcessID

getpid

Process 종료

ExitProcess

exit

Thread 생성

CreateThread

pthread_create

Thread 제거

ThreadExit

pthread_exit

왜 Thread인가?

Process 모델의 문제인 fork의 비효율성을 극복하기 위해 사용된다. 간단히 예를 들어보겠다. 무식한 Web Server 하나가 있다. 이 녀석은 2개 이상의 클라이언트로부터 요청이 오면 자신을 복제하여 새로운 Process를 생성한다. 그리고 이 녀석이 클라이언트 하나를 처리하도록 만든다. 여기서 Process 복사를 위해 fork system call이 발생하는데 동시에 다수의 클라이언트로부터의 요청을 처리하기 위해서는 그만큼의 fork가 일어나고 이는 시스템이 Process 복사를 하느라 다른 일을 처리하지 못하게 되는 상황이 된다. 매우 비효율적이다. 다른 이유를 찾아보면 멀티프로세싱(multi-processing)을 위해서이다. 하나의 Process는 하나의 프로세서(CPU)에서 동작하게 된다. 시스템이 자동적으로 Process를 병렬화 하는 것은 불가능 하기 때문에 멀티프로세싱이 가능한 시스템에서는 단일 Process로 동작하는 것은 매우 비효율적이다. 이러한 비효율성을 해결하기 위해서 Thread 가 등장한다.

Process, Thread 두 모델간의 차이

  • Process 모델은 heavy-weight 모델이라고 표현할 수 있다. Process를 복사할 때 Process가 가지고 있는 모든 자료구조를 다시 생성하고 복사하는 등 이로 인해 발생하는 비용이 크다. 반면 Thread 모델은 light-weight 모델이다. Process의 대부분의 내용을 공유하며 Thread를 위한 자료구조, 지역변수, register 저장을 위한 공간, stack, PC 등만 필요할 뿐이다.
  • 전통적인 Process 자료구조 = Process Context + (data, code, stack), Process Context = Program Context + Kernel Context
  • 현대의 Process 자료구조 = Thread + (code, data) + Kernel context, Thread = Thread context + User stack
  • 각각의 Thread는 자신만의 logical control flow, 즉 PC(Program Counter)를 가진다.
  • 각각의 Thread는 동일한 code, data, Kernel context를 공유한다.
  • 각각의 Thread는 자신의 Thread ID를 가진다.
  • 논리적 구조가 Tree 형태가 아니다. Process로부터 파생이 되기는 하지만 모든 Thread는 동일한 Level의 Child 일 뿐이다. 반면 Process의 복제에서는 Tree처럼 이루어진다.
  • OS가 Swap out을 할 때 Process 단위로 이루어진다. Thread의 경우 Process가 Swap out되면 함께 되는 것이므로 따로 Swap out의 대상이 되지 않는다.
  • 하나의 Thread는 하나의 Process에만 속하게 된다. 그러나 하나의 Process는 여러 개의 Thread를 가질 수 있다.

간단히 정리한 Process와 Thread의 공통점과 차이점

  • 공통점: Thread와 Process는 스케줄링의 단위가 된다(context switch). 그리고 각각은 logical control flow인 PC를 가지며 동시에 동작한다.
  • 차이점: Thread는 code, data를 공유하지만 Process는 그렇지 않다. 그리고 생성과 context switching, 종료의 관점에서 Thread가 훨씬 비용이 적게 든다.

Thread의 이점

생성, 종료, context switching 비용이 적어 경제적이다. 리소스 공유를 통한 향상된 통신을 할 수 있다. 즉, Process와는 다르게 Kernel의 간섭 없이 Thread 간의 빠르게 정보교환을 할 수 있다. Process의 경우 IPC따위를 통해야 하기 때문에 복잡하다. 마지막으로 멀티프로세서 환경에서 매우 유용하다. 각각의 Thread는 다른 프로세서에서 병렬적으로 동작할 수 있다.

멀티 쓰레드 프로그램 설계를 위한 8가지 규칙

'The Art of Concurrency' 책의 챕터 4 영문을 번역한 글입니다.

  1. 완전히 독립적인 계산을 식별
  2. 가능한 가장 높은 수준에서 동시성 구현
  3. 증가하는 코어 수를 활용하기 위한 확장성 조기 계획
  4. 가능하면 스레드 안전 라이브러리 사용
  5. 올바른 스레딩 모델 사용
  6. 특정 실행 순서를 가정하지 마십시오
  7. 가능한 경우 스레드 로컬 스토리지 사용 또는 특정 데이터에 Lock 설정
  8. 동시성 향상을위한 알고리즘 변경

User Level, Kernel Level Threads

User Level Thread
Library의 link를 통해서 Thread를 관리한다. 라이브러리는 Kernel의 축소판이다. Thread를 컨트롤하기 위해 사용한다. Kernel의 도움 없이 동기화, 생성, 스케줄링을 할 수 있다. 즉, Kernel의 리소스를 사용하지 않는다. 그러나 Process와 동일한 주소공간을 사용한다. 그리고 User stack과 register 등의 context를 따로 저장하고 있다. 그러나 하나의 Process가 Thread를 관리하는 구조가 되므로 멀티 프로세서를 지원하지 못하는 문제점과 Thread가 Block될 경우 Process 전체가 Block되는 문제가 있다.
Kernel Level Thread (system call을 통해 Thread를 관리한다)
Kernel이 Process와 Thread를 위한 context 정보를 유지하고 있다. Process와는 독립된 스케줄링을 할 수 있으며 Kernel의 표준 동기화 메커니즘을 사용할 수 있다. Thread가 Block되어도 Process가 Block되지 않으며 멀티프로세서를 지원한다. 그러나 User-Level Thread에 비해 느리고 무겁다. Thread 생성, 스케줄링, 동기화 등을 하기 위해 system call이 사용 되기 때문이다. 또한 Thread 정보가 Kernel에 저장되기 때문에 Kernel 리소스의 제약에 의해 Thread수의 제한이 생긴다.

Threading Models

  • Many-to-one: 여러 개의 Thread가 하나의 Kernel Thread로 동작한다. 하나의 Kernel Thread는 Process가 될 수 있다. 즉, User-Level Thread 만으로 구성되는 모델이다. User Level Thread의 장단점을 그대로 가지고 있다.
  • One-to-one: Kernel Level Thread만으로 구성되는 모델이다. Kernel Level Thread의 장단점을 그대로 가지고 있다. (Win 2k, NT, OS/2 등)
  • Many-to-Many: Many-to-one + one-to-one 의 형태이다. 많은 User Level Thread가 하나의 Kernel Level Thread에서 동작한다. 그리고 Process당 여러 개의 Kernel Level Thread가 생성 될 수 있다. Thread 의 생성은 User space에서 완료되며 멀티프로세서를 지원한다. 또한 block system call이 발생해도 전체 Process가 block 되지 않는다. (Solaris HP-UX, IRix 등)

Threading Issues

  • Fork Issue: Thread 내에서 fork가 일어나면 모든 Thread를 복사해야 하는가 아니면 새로운 Process에 해당 Thread만 생성하고 말아야 하는가?
  • Cancellation Issue :할당 받은 메모리가 남아있거나 다른 Thread와 공유중인 데이터가 업데이트 중일 때 말소될 경우 어떻게 처리해야 하는가?
  • Signal handling Issue : signal이 발생했을 때 이를 처리할 Thread는? 즉 signal을 모든 Thread에게 보내주어야 하나 아니면 특정 Thread에만 보내주어야 하나? 참고로 Solaris의 경우 모든 Thread에서 Signal을 처리하도록 하고 있다.
  • Thread polling Issue : Process가 시작하면 가능한 수만큼 Thread를 생성하여 pool에 보관했다가 작업이 필요하면 사용한다.
  • Thread Interface Issue : Vender에 의존적이다. 즉 OS마다 다르다. 그러나 pThreads 같이 POSIX 표준을 지원할 경우 동일한 인터페이스로 Thread를 관리할 수 있다.

Terminate Thread

스레드를 종료시키기 위한 방법 중 바람직한 방법 순서로 설명한다.

  1. 스레드 함수 반환. (가장 바람직함)
  2. 스레드 함수내의 스레드 종료 함수 호출 (Win32의 경우 ExitThread() 호출).
  3. 동일한 프로세스 또는 다른 프로세스의 원격 종료 (Win32의 경우 TerminateThread() 호출).
  4. 스레드가 포함된 프로세스 종료.

Win32 스레드 종료관련 이슈

CloseHandle 호출이유:

스레드 핸들을 사용하지 않을 경우 보통 스레드생성 직후 CloseHandle()을 호출한다.
스레드 생성 직후 CloseHandle()을 호출한다고 해서 스레드를 사용하지 못하는 것은 아니다.
윈도우는 핸들을 커널 오브젝트로서 레퍼런스 카운트로 관리되고 있다. (마치 DLL처럼)
스레드의 경우 레퍼런스 카운트는 생성되며 +1, 함수가 호출되며 +1된다.
(총 +2) 동일하게 스레드 함수가 리턴하게 되면 -1, CloseHandle()을 사용해도 -1된다.
최종적으로 레퍼런스 카운트가 0이 될 경우 스레드는 자동으로 없어진다.
따라서 이 핸들에 대해서 조정할 필요가 없을 경우, 생성직후 바로 CloseHandle()을 호출해서 레퍼런스 값을 줄여주는 것이 좋다.

TerminateThread 유의사항:

TerminateThread()는 비동기 함수이다. 따라서 이 함수를 호출하여 스레드를 종료할 경우 함수가 반환되기 이전에 해당 스레드가 종료되었음을 보장할 수 없다.
정확한 스레드 종료 시점을 알고싶다면 WaitForSingleObject()를 통하여 스레드의 종료를 기다린다.
스레드는 자신이 종료될것이라는 사실을 전달받지 못하기 때문에 적절한 정리작업을 수행하지 못한다.
시스템은 종료된 스레드를 소유하고 있던 프로세스가 살아있는 동안 그 스레드가 사용했던 스레드 스택을 정리하지 않는다.

Win32 스레드 중지/재개 방법

스레드를 중지 및 재개할 경우 아래와 같은 유의사항이 있다.

  • 스레드를 중지/재개할 경우 suspend count 변수가 증가/감소된다.
  • SuspendThread를 호출할 경우 suspend count 변수가 +1 되며,
  • ResumeThread를 호출할 경우 suspend count 변수가 -1 된다.
  • 스레드는 suspend count가 0일 경우만 작동된다.

Thread priority

스레드의 실행 우선순위와 관련된 이슈 및 그 방법에 대하여 설명한다.

SetThreadPriority, GetThreadPriority
Win32 스레드 우선순위를 확인하고 설정한다.
SetPriorityClass, GetPriorityClass
Win32 프로세스 우선순위를 확인하고 설정한다.

OS별 Thread 구조

  • Solaris: Many-to-many model 이다. 여러 개의 LWP(Light Weight Process)에 여러 개의 User Level Thread가 동작한다. LWP는 Kernel Level Thread 이다. User Level Thread는 하나의 LWP에 bound된다. User Level Thread는 Kernel의 간섭 없이 Thread 라이브러리를 통해 스케줄링 된다. 각각의 Process는 최소 하나의 LWP를 가진다. LWP의 자료구조는 Kernel에서 유지하고 있다.
  • Window 2k: one-to-one model 이다.
  • Linux Thread: 멀리스레딩을 지원한다. 그러나 효율적인 Kernel레벨의 멀티스레딩을 지원하지는 않는다. 대신 LWP와 비슷한 형태를 제공한다. 특징이라고 하면 Process와 Thread를 구분하지 않는다는 것이다. Task_struct 하나만을 사용하고 있다. LWP는 clone() system call을 통해 만들어 진다. Fork와 유사하다. Process의 복사본을 만드는 것 대신 parent task의 주소공간을 공유하는 분리된 Process를 생성한다.

Thread model

커널 레벨 쓰레드의 장점 및 단점

  • 장점: 커널에서 직접 제공해 주기 때문에 안전성과 다양한 기능성이 제공된다.
  • 단점: 커널에서 제공해 주는 기능이기 때문에 유저 모드에서 커널 모드로의 전환이 빈번하게 일어난다. 따라서 이는 성능의 저하로 이어지게 된다.

유저 레벨 쓰레드의 장점 및 단점

  • 장점: 커널은 쓰레드의 존재를 모르기 때문에, 유저 모드에서 커널 모드로의 전환이 필요 없다. 그래서 성능이 좋다.
  • 단점: 운영체제는 프로세스의 존재만 알고 쓰레드의 존재를 모른다. 그래서 프로세스 내의 쓰레드 문제가 발생하지 않게 프로그래밍을 해야 하기 때문에 프로그래밍이 어렵고 커널 레벨 쓰레드에 비해서 결과 예측이 어렵게 된다.

See also

Favorite site

References


  1. Windows_process_and_thread.zip 

  2. C_-_reentrant_function_and_thread-safe_function.pdf 

  3. C++11_threads_-affinity_and_hyperthreading-_Eli_Bendersky's_website.pdf 

  4. Top_20_Cpp_multithreading_mistakes_and_how_to_avoid_them_-_A_CODER'S_JOURNEY.pdf 

  5. C11_vs_Cpp11_vs_POSIX_Thread_API_comp_-_jacking75.pdf