노무현 대통령 배너


2006. 6. 30. 17:33

POSIX 쓰레드로 멀티 쓰레드 프로그래밍하기

v1.1.1

POSIX 쓰레드로 멀티 쓰레드

프로그래밍하기

옮긴이: 차현진(terminus@kldp.org)
원 본 :
http://users.actcom.co.il/~choo/lupg/tutorials/multi-thread/multi-thread.html

번역본: http://terminus.pe.kr/doc/thread/multi-thread.html

차례

  1. 시작하기 전에(Before We Start)...
  2. 쓰레드가 뭔데 그걸 쓰죠?(What Is a Thread? Why Use Threads)
  3. 쓰레드 만들고 없애기(Creating And Destroying Threads)
  4. 뮤텍스로 쓰레드 동기화하기(Synchronizing Threads With Mutexes)
    1. 뮤텍스가 뭐죠?(What Is A Mutex?)
    2. 뮤텍스 만들고 초기화하기(Creating And Initializing A Mutex)
    3. 뮤텍스 걸고 풀기(Locking And Unlocking A Mutex)
    4. 뮤텍스 없애기(Destroying A Mutex)
    5. 뮤텍스 사용법 - 완전한 예제(Using A Mutex - A Complete Example)
    6. 굶어죽기와 데드락(Starvation And Deadlock Situations)
  5. 세련된 동기화 - 조건 변수(Refined Synchronization - Condition Variables)
    1. 조건 변수가 뭐죠?(What Is A Condition Variable?)
    2. 조건 변수 만들고 초기화하기(Creating And Initializing A Condition Variable)
    3. 조건 변수에 시그널 보내기(Signaling A Condition Variable)
    4. 조건 변수 기다리기(Waiting On A Condition Variable)
    5. 조건 변수 없애기(Destroying A Condition Variable)
    6. 실제 상황의 조건 변수(A Real Condition For A Condition Variable)
    7. 조건 변수 사용법 - 완전한 예제(Using A Condition Variable - A Complete Example)
  6. "Private" thread data - Thread-Specific Data
    1. Overview Of Thread-Specific Data Support
    2. Allocating Thread-Specific Data Block
    3. Accessing Thread-Specific Data
    4. Deleting Thread-Specific Data Block
    5. A Complete Example
  7. 쓰레드 취소와 끝내기(Thread Cancellation And Termination)
    1. 쓰레드 취소하기(Canceling A Thread)
    2. 쓰레드 취소 상태 설정하기(Setting Thread Cancellation State)
    3. 취소 위치(Cancellation Points)
    4. 쓰레드 청소 함수 설정하기(Setting Thread Cleanup Functions)
    5. 쓰레드 끝내기 동기화(Synchronizing On Threads Exiting)
    6. 쓰레드 떼어내기(Detaching A Thread)
    7. 쓰레드 취소 - 완전한 예제(Threads Cancellation - A Complete Example)
  8. 쓰레드를 이용한 사용자 인터페이스 프로그래밍(Using Threads For Responsive User Interface Programming)
    1. 사용자 인터페이스 - 완전한 예제(User Interaction - A Complete Example)
  9. 멀티 쓰레드 어플리케이션에서 비시스템 라이브러리 쓰기(Using 3rd-Party Libraries In A Multi-Threaded Application)
  10. 쓰레드를 지원하는 디버거 쓰기(Using A Threads-Aware Debugger)


시작하기 전에(Before We Start)...

이 튜토리얼은 여러분에게 POSIX 쓰레드(pthread)를 이용한 멀티 쓰레드 프로그램에 익숙해지게 하고 쓰레드의 특징들이 실제 프로그램에서 어떻게 쓰이는지 보여줄 것입니다. 라이브러리가 정의해 놓은 여러가지 툴들을 설명하고 그것들을 어떻게 쓰는지, 또한 프로그래밍 문제를 해결하기위해 실제로 어떻게 적용시키는지를 보여줄 것입니다. 이 글을 읽으려면 병렬 프로그래밍(혹은 멀티 프로세스) 개념을 알고 있어야 합니다. 안 그러면 개념 잡기가 약간 힘들 것입니다. 각 튜토리얼은 "직렬" 프로그래밍에만 익숙한 독자들을 위해 이론적 배경 지식과 용어들을 설명 하면서 시작할 것입니다.

독자들이 X나 모티프 같은 비동기적인 프로그래밍 환경에 익숙하다고 가정을 하고 진행하겠습니다. 이런 환경에 익숙하다면 멀티 쓰레드 프로그래밍 개념을 이해하기 쉽습니다.

POSIX 쓰레드를 말할 때 항상 나오는 질문은 "어떤 POSIX 쓰레드 표준안을 써야 할 것인가?"입니다. 쓰레드 표준은 지난 몇 년간 계속 수정중이기 때문에 서로 다른 함수들, 서로 다른 디폴트 값, 서로 다른 뉘앙스의 여러 구현들이 있습니다. 본 튜토리얼은 리눅스 시스템의 커널 레벨 LinuxThreads 라이브러리 0.5 버전을 사용했기 때문에 다른 시스템, 다른 버전의 pthread를 쓰는 프로그래머들은 문제 발생시 해당 시스템의 매뉴얼을 참고해야 할 것입니다. 몇몇 예제들은 블러킹 시스템 콜을 쓰기 때문에 유저 레벨 쓰레드 라이브러리에서는 동작하지 않을 것입니다 (더 많은 정보를 보려면 우리 웹 사이트의 parallel programming theory tutorial을 참고하세요).
앞에서 얘기 했듯이 여기 나오는 예제들은 리눅스 이외의 다른 시스템에서도 동작하도록 노력을 했습니다(솔라리스 2.5).


쓰레드가 뭔데 그걸 쓰죠?(What Is a Thread? Why Use Threads)

쓰레드는 프로세스와 비슷합니다. 자신의 스택을 가지고 주어진 코드를 실행합니다. 하지만 진짜 프로세스와는 다르게 메모리를 다른 쓰레드와 공유합니다(프로세스는 자신만의 메모리 공간을 가지고 있습니다). 쓰레드 그룹은 한 프로세스 안에서 실행되는 모든 쓰레드를 나타내고, 메모리를 공유하기 때문에 전역 변수와 힙 메모리, 파일 디스크립터 등등을 공유합니다. 또한 같은 쓰레드 그룹의 쓰레드들은 병렬적으로 실행됩니다(즉, 시간을 잘라서 사용을 하는데 프로세서가 여러개라면 진짜 병렬로 동작합니다).

보통의 순차적인 프로그램 대신 쓰레드 그룹을 사용하면 몇 가지 일을 동시에 할 수 있는 장점이 있습니다. 따라서 어떤 이벤트에 대해 즉각적으로 반응을 할 수 있습니다 (예를 들면, 한 쓰레드는 사용자 인터페이스를 처리하고 다른 쓰레드는 데이타베이스 쿼리를 처리한다고 하면, 아주 엄청난 양의 쿼리가 들어와 바쁜 경우에도 사용자 입력에 대해 반응하고 처리할 수가 있습니다).

프로세스 그룹대신 쓰레드 그룹을 사용했을 때의 장점으로는 쓰레드간 컨택스트 스위치(context switching)가 프로세스간 컨택스트 스위치보다 훨씬 빠르다는 것입니다(컨택스트 스위칭이란 현재 돌고 있는 쓰레드나 프로세스에서 다른 쓰레드나 프로세스로 옮겨 가는 것을 말합니다). 또한, 보통 두 쓰레드간 통신을 두 프로세스간 통신보다 빠르고 쉽게 구현 할 수 있습니다.

다른 한 편으로는 한 그룹안의 모든 쓰레드들은 같은 메모리 영역을 사용하기 때문에 한 쓰레드가 메모리를 잘 못 건드리면 다른 쓰레드들에 영향이 미칠 수 있습니다. 프로세스에서는 운영체제가 프로세스를 다른 프로세스로부터 보호해 주기 때문에 쓰레드같은 영향은 없습니다. 프로세스의 다른 장점으로, 서로 다른 프로세스는 서로 다른 시스템(머신)에서 각각 돌 수 있다는 것입니다. 쓰레드는 보통 한 시스템에서 돌아야 합니다.


쓰레드 만들고 없애기(Creating And Destroying Threads)

멀티 쓰레드 프로그램이 실행을 시작하면 main()을 실행시키는 하나의 쓰레드만이 존재하게 됩니다. 이 완전한 쓰레드는 자신의 쓰레드 ID를 갖습니다. 새 쓰레드를 만들려면 pthread_create() 함수를 써야 됩니다. 어떻게 쓰는지 보시죠.


#include /* 표준 I/O 루틴 */#include /* pthread 함수와 데이타 스트럭쳐 *//* 새 쓰레드가 실행시킬 함수 */void*do_loop(void* data){ int i; int i; /* 숫자를 찍을 카운터 */ int j; /* 지연용 카운터 */ int me = *((int*)data); /* 쓰레드 구분 숫자 */ for (i=0; i<10; i++) { for (j=0; j<500000; j++) /* 지연 루프 */ ; printf("'%d' - Got '%d'n", me, i); } /* 쓰레드 없애기 */ pthread_exit(NULL);}/* 보통의 C 프로그램처럼 main에서 시작합니다. */intmain(int argc, char* argv[]){ int thr_id; /* 새 쓰레드용 쓰레드 ID */ pthread_t p_thread; /* 쓰레드 구조체 */ int a = 1; /* 1번 쓰레드 구분 숫자 */ int b = 2; /* 2번 쓰레드 구분 숫자 */ /* 'do_loop()를 실행시킬 새 쓰레드 만들기 */ thr_id = pthread_create(&p_thread, NULL, do_loop, (void*)&a); /* main()함수에서도 'do_loop()' 실행시키기 */ do_loop((void*)&b); /* NOT REACHED */ return 0;}

위 프로그램에서 몇 가지를 살펴보겠습니다.

  1. 메인 프로그램 자체도 쓰레드이기 때문에 do_loop()는 자신이 새로 실행시킨 쓰레드가 실행시킨 do_loop()와 병렬로 동작합니다.
  2. pthread_create()는 4개의 파라미터를 받습니다. 첫 번째는 쓰레드에 대한 정보를 제공하기 위해서 쓰입니다. 두 번째는 새 쓰레드에 속성을 주기 위해서 쓰이는데 우리는 NULL 포인터를 넘겨 줘서 기본값을 쓰게 했습니다. 세 번째 파라미터는 어떤 함수에서 쓰레드가 시작할 것인지를 알려주는 것이고 네 번째는 그 함수로 넘겨줄 아규먼트를 나타냅니다. 여기서 'void*'로 캐스팅 한 것은 이것이 비록 ANSI-C 문법에서는 불필요하지만 좀 더 명확하게 하기 위해서 쓰인 것입니다.
  3. 지연 루프는 병렬로 실행되는 쓰레드를 확실히 보여주기 위해서 쓰였습니다. CPU가 너무 빨라서 한 쓰레드가 모두 출력된 다음 다른 쓰레드의 출력이 나온다면 지연값을 증가시키기 바랍니다.
  4. pthread_exit()는 현재 쓰레드를 종료 시키고 자신이 갖고 있던 자신만의 쓰레드 리소스들을 놓아 줍니다. 쓰레드의 첫 함수 마지막에서 꼭 이 함수를 불러야 할 필요는 없습니다. 그 함수에서 리턴을 하게 되면 자동으로 종료가 됩니다. 쓰레드 중간에서 쓰레드를 종료하고 싶은 경우가 생길 때, 유용하게 쓰일 수 있습니다.

멀티 쓰레드 프로그램을 gcc로 컴파일 하려면 pthread 라이브러리 를 링크시켜줘야 합니다. 이미 여러분의 시스템에 이 라이브러리가 설치되어 있다고 가정하고 어떻게 컴파일 하는지를 보여 드리겠습니다.

gcc pthread_create.c -o pthread_create -lpthread

앞으로 나올 몇몇 프로그램들은 제대로 컴파일 하기 위해서 '-D_GNU_SOURCE' 를 줘서 컴파일 해야 할지도 모릅니다. 주의하세요.

이 프로그램의 소스 코드는
pthread_create.c를 보세요.


뮤텍스로 쓰레드 동기화하기

여러개의 쓰레드를 동시에 돌릴 때 발생하는 기본적인 문제점 중의 하나는 같은 메모리 영역을 쓰기 때문에 "서로의 상태에 신경 쓰도록" 하는 것입니다. 그래서 여기서는 두 개의 쓰레드가 동일한 데이타 구조에 접근할 때 생기는 문제점을 살펴 보도록 하겠습니다.

예를 들어서, 두 쓰레드가 두 변수를 업데이트 하려고 하는 상황을 생각해 봅시다. 한 쓰레드는 두 변수를 0으로 세트하려고 하고, 다른 쓰레드는 두 변수를 1로 세트 하려고 합니다. 만약에 두 쓰레드가 동시에 이 일을 하려고 한다면 한 변수는 1로, 다른 한 변수는 0으로 세트된 상황이 생길 수도 있습니다. 이런 일이 생기는 이유는 첫번째 쓰레드가 첫번째 변수를 0으로 만들고 나서 바로 컨택스트 스위치(context switching-이제 이게 뭔지 아시죠?)가 일어나고, 두번째 쓰레드가 두 변수를 1로 세트를 한 다음 다시 첫번째 쓰레드가 동작을 하면 두 번째 변수만을 0으로 만들기 때문에 결과적으로 첫 번째 변수는 1로, 두 번째 변수는 0으로 됩니다.


뮤텍스(mutex)가 뭐죠?

이 문제를 해결하기 위해서 pthread 라이브러리가 제공하는 기본 메카니즘을 뮤텍스라 부릅니다. 뮤텍스는 다음 세가지를 보장해주는 잠금 장치입니다. (역주: mutex - MUTual EXclusion - 상호 배타성)

  1. 원자성(Atomicity) - 뮤텍스를 걸었을 경우 다른 쓰레드가 동시에 뮤텍스가 걸린 영역으로 들어오지 못하게 보장해주는 원자적 동작입니다.
  2. 유일성(Singularity) - 한 쓰레드가 뮤텍스를 걸었을 경우 자신이 풀기 전에는 다른 쓰레드가 다시 뮤텍스를 걸지 못하게 해 줍니다.
  3. Non-Busy Wait - A라는 쓰레드가 이미 뮤텍스가 걸린 B 쓰레드를 걸려고 한다면 A 쓰레드는 B 쓰레드가 뮤텍스를 풀 때까지 서스펜드(suspend)됩니다 (CPU 리소스를 전혀 사용하지 않습니다). B가 뮤텍스를 풀면 A는 깨어나고 자신이 뮤텍스를 걸고 실행을 계속해 나갑니다.

이 세가지에서 볼 수 있듯이 어떻게 뮤텍스가 변수(혹은 코드의 임계 부분)에 대해서 배타적 접근을 확실하게 해 주는지 알 수 있습니다. 앞에서 설명했던 두 변수를 업데이트 해주는 가상 코드를 살펴 보죠. 다음은 첫 번째 쓰레드입니다.

'X1' 뮤텍스를 잠근다.첫번째 변수를 '0'으로 세팅.두번째 변수를 '0'으로 세팅.'X1' 뮤텍스를 푼다.

두번째 쓰레드는 이렇게 되겠죠.

'X1' 뮤텍스를 잠근다.첫번째 변수를 '1'로 세팅.두번째 변수를 '1'로 세팅.'X1' 뮤텍스를 푼다.

두 쓰레드가 같은 뮤텍스를 쓰고 동시에 돌았다고 하면 두 변수 모두 '0'으로 세트되어 있던지 '1'로 세트되어 있을 겁니다. 프로그래머가 주의할 일이 조금 있습니다. 만약에 세번째 쓰레드가 코드의 다른 부분에서 'X1' 뮤텍스 없이 이 두 변수에 접근을 한다면 역시나 변수 내용이 뒤죽박죽 될 가능성이 있습니다. 따라서 이 변수에 접근하는 모든 코드들을 조그만 함수로 만들어 놓고 이 변수들에 접근 할 때는 이 함수만 쓰도록 해야 합니다.


뮤텍스 만들고 초기화하기

뮤텍스를 만들려면 먼저 pthread_mutex_t 형의 변수를 선언하고 초기화 해야 합니다. 가장 간단한 방법은 PTHREAD_MUTEX_INITIALIZER 상수를 할당하는 것입니다. 따라서 다음같은 코드를 쓰면 되겠습니다.

pthread_mutex_t a_mutex = PTHREAD_MUTEX_INITIALIZER;

주의 할 점이 하나 있는데, 이런 형태의 초기화는 '빠른 뮤텍스(fast mutex)'라는 뮤텍스를 만들어 줍니다. 무슨 뜻이냐면, 만약에 쓰레드가 뮤텍스를 잠근 뒤에 또, 그 뮤텍스를 잠그려고 하면, 그냥 멈춰버릴 것입니다. - 데드락(deadlock)이 걸린다는 뜻입니다.

'재귀적 뮤텍스(recursive mutex)'란 다른 형태도 있는데 한 번 잠근 뒤에도 몇 번이고 더 잠글 수 있게 해주는 뮤텍스입니다. 이 뮤텍스는 위에서 말한 데드락 상황이 안 걸리게 해 줍니다(하지만 이 뮤텍스를 풀려고 하는 다른 쓰레드는 멈출 것입니다). 걸었던 뮤텍스를 풀 때, 걸었던 만큼 풀지 않는 한 뮤텍스가 계속 걸려 있을 겁니다. 이 방법은 현대적인 문잠금 장치에서 문을 잠글 때는 시계 방향으로 두 번 돌리고, 풀 때는 반시계 방향으로 두 번 돌리는 것과 비슷합니다. 이런 뮤텍스를 만들려면 PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP를 할당해 주면 됩니다.


뮤텍스 걸고 풀기(Locking And Unlocking A Mutex)

뮤텍스를 걸때는 pthread_mutex_lock() 함수를 씁니다. 뮤텍스를 걸려고 하는데, 이미 다른 쓰레드가 그 뮤텍스를 걸어놨다면 자신의 쓰레드를 멈추게 합니다. 이렇게 멈췄을 경우에는 뮤텍스를 걸었던 프로세스가 뮤텍스를 풀면 이 함수는 다시 뮤텍스를 걸고 리턴을 합니다. 미리 초기화 했다고 가정하고 어떻게 뮤텍스를 거는지 보여 드리죠.

int rc = pthread_mutex_lock(&a_mutex);if (rc) { /* 에러 발생 */ perror("pthread_mutex_lock"); pthread_exit(NULL);}/* 뮤텍스가 걸렸습니다. 필요한 일을 하세요. */..

쓰레드는 자신이 할 일(변수나 데이타 구조의 값을 바꾼다거나 파일을 처리하는등)을 하고 나면 다음처럼 pthread_mutex_unlock() 함수를 써서 뮤텍스를 풀어 줘야 합니다.

rc = pthread_mutex_unlock(&a_mutex);if (rc) { perror("pthread_mutex_unlock"); pthread_exit(NULL);}


뮤텍스 없애기(Destroying A Mutex)

뮤텍스로 할 일을 다 했다면 이젠 없앨 차례입니다. 할 일을 다 했다는 얘기는 어떤 쓰레드도 그 뮤텍스가 필요없어졌다는 뜻입니다. 만약에 한 쓰레드만 뮤텍스로 할 일을 끝마쳤다면 이 때는 없애면 안 됩니다. 다른 쓰레드가 그 뮤텍스를 쓸지도 모르기 때문입니다. 모든 쓰레드가 확실히 뮤텍스를 쓸 일이 없다면 마지막 쓰레드가 pthread_mutex_destroy() 함수로 그 뮤텍스를 없앨 수 있습니다.

rc = pthread_mutex_destroy(&a_mutex);

이 함수를 부르고 나면 a_mutex 변수는 다시 초기화 되지 않는 한 더 이상 뮤텍스로 쓰일 수가 없습니다. 따라서 만약에 한 쓰레드가 너무 일찍 뮤텍스를 없앴을 경우에, 다른 쓰레드에서 잠그거나 풀려고 한다면 잠그고 푸는 함수는 EINVAL 에러 코드를 만나게 됩니다.


뮤텍스 사용법 - 완전한 예제(Using A Mutex - A Complete Example)

뮤텍스의 탄생부터 죽음까지 모두 알아봤기 때문에 이제는 예제를 살펴보겠습니다. 이 예제는 영광스러운 "종업원상"을 타기 위해 다투는 두 종업원을 시뮬레이션합니다. 빠르게 시뮬레이션하기 위해서 3개의 쓰레드를 쓰겠습니다. 하나는 Danny를 "종업원상"에 올리고 두번째 쓰레드는 Moshe를 올립니다. 세번째 쓰레드는 "종업원상"의 내용이 일치하는 지를 보여줍니다(즉, 정확하게 한 종업원의 데이타가 들어있음).
두 개의 프로그램이 있는데 하나는 뮤텍스를 쓰는 것이고 다른 하나는 쓰지 않는 것입니다. 둘 다 해보고 차이점을 알아본 다음, 멀티 쓰레드 환경에서 뮤텍스가 꼭 필요한 이유를 마음으로 느껴 보세요.

이 프로그램들은 파일 형태로 제공됩니다. 뮤텍스를 쓰는 것은 employee-with-mutex.c이고, 뮤텍스를 안 쓰는 것은 employee-without-mutex.c입니다. 소스에 있는 주석을 잘 읽어서 어떻게 동작하는지에 대해서 더 잘 이해하시기 바랍니다.


굶어죽기와 데드락 상황(Starvation And Deadlock Situations)

다시 기억을 되살려 보죠. pthread_mutex_lock()는 이미 잠겨 있는 뮤텍스에 대해서는 알 수 없는 시간 동안 멈춰 있을 수 있습니다. 만약에 그 잠김이 영원하다면 우리의 불쌍한 쓰레드는 "굶어(starved)" 죽습니다. 리소스를 얻으려 하지만 영원히 얻지 못하게 되는 것입니다. 이런 굶어 죽기( starvation)가 발생하지 않도록 하는 것은 프로그래머에게 달려 있습니다. pthread 라이브러리는 어떤 도움도 줄 수가 없습니다.

그렇지만, pthread 라이브러리는 "데드락(deadlock)"은 해결 할 수도 있습니다. 데드락이란 모두 같은 상태인 몇몇 쓰레드가 다른 쓰레드가 갖고 있는 리소스를 기다리는 상황입니다.(A deadlock is a situation in which a set of threads are all waiting for resources taken by other threads, all in the same set.) 당연히 모든 쓰레드가 뮤텍스를 기다리면서 멈춰있다면 아무도 다시 돌 수는 없을 것입니다. pthread 라이브러리는 이런 상황을 추적하다가 마지막 쓰레드가 pthread_mutex_lock()를 부르면 실패를 리턴하면서 EDEADLK 에러를 발생시킵니다. 프로그래머는 이런 값을 확인해서 데드락을 피할 방법을 찾아야 합니다.


세련된 동기화 - 조건 변수(Refined Synchronization - Condition Variables)

지금까지 살펴본 뮤텍스는 리소스에 대한 배타적 접근이라는 간단한 동기화를 제공합니다만, 가끔은 진짜 동기화가 필요할 경우가 있습니다.

  • 서버에서, 한 쓰레드는 클라이언트의 요청을 읽어들이고 그 요청을 해석해서 여러 쓰레드에게 처리를 넘깁니다. 이 처리 쓰레드들은 처리할 데이타가 생길 경우에 그 사실을 알아야 할 필요가 있습니다. 그렇지 않다면 CPU 시간을 쓰지 않으면서 기다려야 합니다.
  • GUI(Graphical User Interface) 어플리케이션에서 한 쓰레드는 사용자 입력을 읽어 들이고 한 쓰레드는 그래픽 출력을 담당하며, 한 쓰레드는 서버에 요청을 보내고 그 응답을 처리합니다. 서버쪽을 담당하는 쓰레드는 서버에서 응답이 왔을 때 그래픽을 담당하는 쓰레드에게 알려줄 수가 있어야 합니다. 그래야 사용자에게 즉시 보여줄 수 있기 때문입니다. 사용자 입력 담당 쓰레드는 예를 들면 서버 담당 쓰레드가 아주 긴 동작중이더라도 사용자가 그것을 취소 시킬 수 있게 해주는 상황처럼 사용자의 요청에 항상 빠르게 응답해야 할 필요가 있습니다.
이 상황들은 모두, 쓰레드는 서로 어떤 사건에 대해서 상대방에게 통보할 수 있는 능력이 필요합니다. 이것이 바로 조건 변수가 탄생한 이유입니다.


조건 변수가 뭐죠?(What Is A Condition Variable?)

조건 변수는 어떤 일이 발생할 때까지 CPU 사이클을 낭비하지 않고 기다릴 수 있도록 해 주는 메카니즘입니다. 몇개의 쓰레드가 조건 변수를 기다리고 있고, 다른 쓰레드가 그 조건 변수에 대해서 시그널을 날려주면(사건을 통지) 기다리던 쓰레드중의 하나가 깨어나서 그 사건에 대해 반응을 하게 됩니다. 또한 그 조건 변수를 기다리고 있던 모든 쓰레드를 깨울 수 있게 브로드캐스트 할 수 있는 방법도 있습니다.

주의할 점은 조건 변수는 잠금을 지원하지 않는다는 것입니다. 따라서 조건 변수에 접근을 하려면 뮤텍스와 같이 사용을 해야 합니다.


조건 변수 만들고 초기화하기(Creating And Initializing A Condition Variable)

조건 변수를 만들려면 pthread_cond_t 형의 변수를 선언하고 알맞게 초기화 시켜줘야 합니다. 초기화는 간단하게 PTHREAD_COND_INITIALIZER 라는 매크로를 쓰던지, pthread_cond_init() 함수를 쓰면 됩니다. 매크로를 쓰는 예제를 살펴 보겠습니다.

pthread_cond_t got_request = PTHREAD_COND_INITIALIZER;

'got_request'라는 조건 변수를 선언하고 초기화 합니다.

주의사항: PTHREAD_COND_INITIALIZER는 실제로 구조체이기 때문에 조건 변수가 선언 될 때에만 쓰일 수 있습니다. 실행 시간에 초기화를 해야 한다면 pthread_cond_init()함수를 쓰기 바랍니다.


조건 변수 시그널 날리기(Signaling A Condition Variable)

조건 변수에 시그널을 날리는 방법은 두 가지가 있습니다. 하나는 pthread_cond_signal() 함수를 부르는 것이고(이 변수를 기다리고 있는 하나의 쓰레드만을 깨울 때), 또 하나는 pthread_cond_broadcast() 함수를 부르는 것입니다(이 변수를 기다리고 있는 모든 쓰레드를 깨울 때). 'got_request'가 적당히 초기화 됐다고 가정하고 예제를 살펴보도록 하죠.

int rc = pthread_cond_signal(&got_request);

혹은 브로드캐스트 함수를 써서,

int rc = pthread_cond_broadcast(&got_request);

두 함수 모두 성공했을 때는 'rc'를 0으로, 실패했을 때는 0이 아닌 값으로 세팅합니다. 실패 했을 경우에는 리턴값은 에러 이유를 나타냅니다(파라미터가 조건 변수가 아닐 때는 EINVAL를, 시스템 메모리가 부족할 때는 ENOMEM를 나타냅니다).

주의 사항: 시그널이 성공했다고 해서 어떤 쓰레드가 깨어났다는 뜻은 아닙니다. 그 조건 변수를 기다리던 쓰레드가 하나도 없었다면 아무일도 아닌 것이죠(즉, 시그널을 잃어버리는 것입니다).
그리고 시그널을 저장해놨다가 쓸 수도 없습니다. 만약에 시그널 함수가 리턴한 다음에 어떤 쓰레드가 그 조건 변수를 기다리기 시작한다면 그 쓰레드는 다른 시그널이 발생해야 깨어날 수 있습니다.


조건 변수 기다리기(Waiting On A Condition Variable)

어떤 쓰레드가 조건 변수에 시그널을 날리길 다른 쓰레드가 기다리려고 한다면 다음 두 함수 중에 한 함수를 쓰면 됩니다. pthread_cond_wait(), pthread_cond_timedwait(). 각 함수는 조건 변수와 뮤텍스(기다리기 전에 뮤텍스를 걸지도 모르기 때문에)를 넘겨 받아서 뮤텍스를 푼 다음에 조건 변수에 시그널이 들어올 때까지 잠들어 버립니다. 앞에서 살펴 봤던 pthread_cond_signal()에 의해서 시그널이 발생해, 깨어 나게 된다면 뮤텍스는 자동으로 다시 잠기고 리턴하게 됩니다.

두 함수가 다른 점은 pthread_cond_timedwait()에 기다릴 시간을 알려준다는 것인데 ETIMEDOUT의 에러값을 갖고 리턴을 해서 조건 변수가 시그널을 받은 것이 아니라 시간이 지나서 리턴했다는 것을 알려준다는 것입니다. pthread_cond_wait() 는 시그널을 받기 전에는 영원히 기다릴 것입니다.

두 함수를 어떻게 쓰는지 보여드리죠. 'got_request'는 적당한 조건 변수로 초기화 됐고 역시 'request_mutex'도 적당한 뮤텍스로 초기화 됐다고 가정합니다. 먼저 pthread_cond_wait() 함수를 봅시다.

/* 뮤텍스를 먼저 걸고 */int rc = pthread_mutex_lock(&a_mutex);if (rc) { /* 에러 났음 */ perror("pthread_mutex_lock"); pthread_exit(NULL);}/* 이제 뮤텍스가 걸렸고, 조건 변수를 기다린다. *//* pthread_cond_wait이 실행되는 동안 뮤텍스는 풀립니다. */rc = pthread_cond_wait(&got_request, &request_mutex);if (rc == 0) { /* 조건 변수가 시그널을 받아서 깨어났습니다. */ /* pthread_cond_wait()가 뮤텍스를 다시 걸어 줍니다. */ /* 할 일을 하세요... */ .}/* 끝으로 뮤텍스를 풀어 줍시다. */pthread_mutex_unlock(&request_mutex);

다음은 pthread_cond_timedwait() 함수를 쓰는 예제입니다.

#include /* struct timeval 정의 */#include /* gettimeofday() 선언 */struct timeval now; /* 기다리기 시작하는 시각 */struct timespec timeout; /* 대기 함수에서 쓸 타임아웃값 */int done; /* 다 기다렸나요? *//* 뮤텍스를 먼저 걸고 */int rc = pthread_mutex_lock(&a_mutex);if (rc) { /* 에러 났음 */ perror("pthread_mutex_lock"); pthread_exit(NULL);}/* 이제 뮤텍스가 걸렸음. *//* 지금 시각을 얻는다. */ gettimeofday(&now);/* 타임아웃값을 세팅 */timeout.tv_sec = now.tv_sec + 5timeout.tv_nsec = now.tv_usec * 1000; /* timeval은 마이크로(micro)초를 씁니다. */ /* timespec은 나노(nano)초를 씁니다. */ /* 1 나노초 = 1000 마이크로초 *//* 조건 변수를 기다림 *//* 유닉스 시그널이 타임아웃 전에 대기 상태를 멈추게 할 수 있기 때문에 루프를 써서 피하겠습니다. */done = 0;while (!done) { /* pthread_cond_timedwait()은 함수 시작부분에서 뮤텍스를 푼다는 것을 기억하세요. */ rc = pthread_cond_timedwait(&got_request, &request_mutex, &timeout); switch(rc) { case 0: /* 조건 변수가 시그널을 받아서 깨어 났음 */ /* pthread_cond_timedwait가 뮤텍스를 다시 걸어줍니다. */ /* 할 일을 하시고... */ . . done = 0; break; case ETIMEDOUT: /* 시간이 다 됐네요 */ done = 0; break; default: /* 에러가 났습니다.(즉, 유닉스 시그널을 받았습니다.) */ break; /* swithc문을 빠져나가지만 다시 while 루프를 돕니다. */ }}/* 자, 끝으로 뮤텍스를 풀어 줍시다. */pthread_mutex_unlock(&request_mutex);

보는바와 같이 타임아웃을 쓰는 버전이 더 복잡합니다. 따라서 필요할 때마다 코드를 만들지 말고 래퍼 함수등을 쓰는게 훨씬 좋을 것입니다.

주의사항: 두 개 이상의 쓰레드가 기다리고 있는 조건 변수가 시그널을 아주 많이 받는다고 할 때, 기다리던 쓰레드 중의 하나는 영원히 깨어 나지 못 할 수도 있습니다. 조건 변수가 시그널을 받았을 때 기다리던 쓰레드중 어떤 쓰레드가 깨어날 지에 대해서 알 수가 없기 때문입니다. 방금 깨어난 쓰레드가 대기 상태로 다시 들어가자마자 시그널이 다시 발생해 그 쓰레드가 다시 깨어나는 식의 동작이 계속 될 수 있기 때문입니다. 이럴 경우에 계속 깨어나지 못하는 쓰레드를 가르켜 "굶어죽었다(starvation)"라고 부릅니다. 이렇게 원치 않는 동작이 일어날 가능성이 있는 상황을 피하는 것은 전적으로 프로그래머의 책임입니다. 하지만 앞에서 봤던 서버 예제에서는 요청이 아주 늦게 들어오고, 서비스 응답을 처리할 쓰레드는 많을 것이기 때문에 아주 바람직한 상황입니다. 즉, 이 경우에는 요청이 발생하자마자 바로바로 처리될 것이기 때문입니다.

주의사항 2: 뮤텍스가 pthread_cond_broadcast로 브로드캐스트를 받았을 때, 그 뮤텍스를 기다리던 모든 쓰레드가 동시에 실행되는것은 아닙니다. 기다리던 각각은 자신의 대기 함수가 리턴하기 전에 뮤텍스를 다시 걸려고 시도를 하기 때문에 하나씩 실행이 됩니다. 즉, 뮤텍스를 걸고, 자기 할 일을 하고, 뮤텍스를 풀고하는 식으로 차례차례 실행이 됩니다.


조건 변수 없애기(Destroying A Condition Variable)

조건 변수를 다 썼다면 없애야겠죠. 이래야 조건 변수가 갖고 있던 시스템 리소스를 반환할테니까요. pthread_cond_destroy()로 이 일을 합니다. 제대로 동작하려면 이 조건 변수를 기다리는 쓰레드가 하나도 없어야 합니다. 사용법을 보여드릴텐데, 역시 'got_request'가 이미 조건 변수로 초기화 되어 있었다고 가정합니다.

int rc = pthread_cond_destroy(&got_request);if (rc == EBUSY) { /* 이 조건 변수를 기다리는 쓰레드가 있군요. */ /* 잘 처리하세요... */ . .}

어떤 쓰레드가 여전히 조건 변수를 기다리고 있다면, 상황에 따라 다르겠지만, 이 조건 변수의 사용에 어떤 허점이 있었을 수도 있고 적당한 쓰레드 종료 코드가 빠졌을 수도 있습니다. 최소한 디버깅 단계에서는 이 상황을 프로그래머에게 알려주는게 좋습니다. 아무 것도 아닐 수도 있고 아주 중대한 결함일 수도 있으니까요.


실제 상황에서의 조건 변수(A Real Condition For A Condition Variable)

조건 변수에 대해서 하나 짚고 가야겠습니다. 이것과 관련된 실제 조건에 대한 확인들이 없다면 조건 변수는 거의 쓸모가 없습니다. 확실히 하기 위해서 앞에서 소개했던 서버 예제를 잠깐 살펴보도록 하죠. 'got_request' 조건 변수가 처리할 새 요청이 들어왔을 때 시그널을 받는다고 가정하고 사용을 했습니다. 이들은 또한 어떤 요청 큐에 들어 있을 것입니다. 그 조건 변수가 시그널을 받았을 때, 기다리던 쓰레드가 있다면 그 쓰레드는 깨어나고 응답을 처리할 것이라는 것을 확신할 수 있습니다.

하지만, 새 요청이 들어온 순간에 모든 쓰레드가 바로 전 응답을 처리하느라 바쁘다면 어떻게 될까요? 이 순간에는 모든 쓰레드는 조건 변수를 기다리고 있지 않고 자기 일을 하고 있었기 때문에 그 조건 변수가 받은 시그널은 무시될 겁니다. 또한 각 쓰레드가 자기 일을 마치고 조건 변수를 기다리는 상태가 됐을 경우, 그 무시됐던 시그널이 다시 발생하지도 않습니다(또다른 새 요청이 없다고 가정하면). 따라서, 모든 쓰레드가 시그널을 기다리느라 멈춰있는 동안 최소한 한 개의 요청이 처리되지 못 하고 남아 있게 됩니다.

이 문제를 해결하기 위해서 요청이 미처리 된 갯수를 정수 변수에 갖고 있겠습니다. 그리고 각 쓰레드는 조건 변수를 기다리기 전에 그 값을 확인해서 그 값이 양수이면 (미처리 된 요청이 있다), 멈추지 않고 그 응답을 처리할 겁니다. 또한, 요청을 처리한 쓰레드는 이 변수를 하나씩 감소시켜야 하는데 이렇게 해야 숫자가 정확해 질것입니다.
이런 고려 사항들이 위에서 봤던 코드를 어떻게 바꾸는지 봅시다.


/* 미처리된 요청, 0으로 초기화 */int num_requests = 0;../* 먼저, 뮤텍스를 잠급시다. */int rc = pthread_mutex_lock(&a_mutex);if (rc) { /* 에러 있음 */ perror("pthread_mutex_lock"); pthread_exit(NULL);}/* 이제 뮤텍스는 잠겼고, 조건 변수를 기다립니다. *//* 처리할 요청이 없다면 */rc = 0;if (num_requests == 0) rc = pthread_cond_wait(&got_request, &request_mutex);if (num_requests > 0 && rc == 0) { /* 미처리 요청이 있네용 */ /* 할 일을 합시다. */ . . /* 미처리 요청수를 하나 줄입니다. */ num_requests--; }}/* 마지막으로, 뮤텍스를 풀어줘야죠 */pthread_mutex_unlock(&request_mutex);

조건 변수 사용법 - 완전한 예제(Using A Condition Variable - A Complete Example)

조건 변수의 실질적인 사용법을 보여주기 위해서 앞에서 설명했던 서버를 시뮬레이션하는 프로그램을 소개하겠습니다. 한 쓰레드는 수신자로서, 클라이언트의 요청을 받아 들여서 링크드 리스트에 요청을 집어 넣습니다. 핸들러 쓰레드는 이 요청을 처리하게 됩니다. 간단하게 하기 위해서 수신자 쓰레드는 실제 클라이언트에서 요청을 받아들이지 않고 자신이 요청을 만들어 내게 할 것입니다.

소스는 thread-pool-server.c에서 볼 수 있습니다. 소스안에 아주 자세한 주석이 달려 있으니까 소스를 먼저 읽어 본 다음에 밑에 나오는 설명을 참고하세요.

  1. 'main' 함수는 먼저 핸들러 쓰레드를 만들고, 자신의 메인 루프를 통해 수신자 쓰레드의 역할을 짊어집니다.
  2. 한 개의 뮤텍스로, 조건 변수와 요청을 기다릴 링크드 리스트, 두 개를 보호하는데 씁니다. 이렇게 하면 전체 설계를 간단하게 할 수 있습니다. 연습문제 하나 내죠. 이 예제를 두 개의 뮤텍스를 쓰는 방식으로 바꿔보세요.
  3. 여기서 쓰이는 뮤텍스는 재귀적 뮤텍스"여야" 합니다. 왜 그런가는 소스 코드중, 'handle_requests_loop' 함수를 보세요. 보면, 먼저 뮤텍스를 걸고, 'get_request' 함수를 부르는데, 여기서도 뮤텍스를 또 거는군요. 만약에 재귀적 뮤텍스를 안 썼다면 이 'get_request' 함수에서 뮤텍스를 거는 순간 영원히 멈춰버릴 것입니다.
    'get_request' 함수에서 뮤텍스 거는 부분을 빼서 두 번 거는 문제를 풀 수 있지 않겠냐라고 할 지도 모르겠지만 이렇게 하면 결함이 있는 설계가 돼 버립니다. 아주 큰 프로그램에서 'get_request'를 다른 코드상에서 부를 수도 있기 때문입니다. 따라서 매번 쓸 때마다 뮤텍스가 적절하게 잠겼는지 확인할 필요가 있습니다.
  4. 일반적으로, 재귀적 뮤텍스를 쓸 때에는, 뮤텍스를 잠그고 푸는 것을 한 함수 안에서 하도록 해야 합니다. 안 그러면, 잠근 수만큼 풀기가 아주 어려워 지고 결국 데드락이 발생하게 될 겁니다.
  5. pthread_cond_wait() 함수가 내부적으로 뮤텍스를 풀었다 다시 거는게 처음에는 헷갈릴 수도 있습니다. 제일 좋은 방법은 코드상에 이런 동작에 대해 주석으로 달아서, 다른 사람이 쓸데없이 뮤텍스를 또 걸지 않게 해 줄 수 있습니다.


개인적인 쓰레드 데이타 - 쓰레드만의 데이타("Private" thread data - Thread-Specific Data)

보통의 쓰레드 하나짜리 프로그램에서 가끔 전역 변수를 써야 할 때가 있습니다. 맞습니다. 나이 드신 훌륭한 선생님께서는 전역 변수를 쓰는게 아주 나쁜 습관이라고 말씀하셨습니다. 하지만 가끔 이게 편할 때가 있습니다. 특히나 한 파일 안에서만 보이는 정적 변수라면 더욱 그렇죠.

멀티 쓰레드 프로그램에서도 이런 전역 변수를 써야 할 경우가 있습니다. 모든 쓰레드에서 접근 가능한 하나의 변수에 대해서는 약간의 오버헤드를 갖는 뮤텍스를 써서 보호해야 한다는 것에 주의하시기 바랍니다. 게다가, 특정한 쓰레드에서만 쓰일 "전역" 변수가 필요할 수도 있고, 똑같은 "전역" 변수이나 다른 쓰레드에서는 다른 값을 가져야 할 때도 있습니다. 예를 들어, 각 쓰레드에서 전역적으로 접근할 수 있는 하나의 연결 리스트(그러나 같지 않은)가 필요하다고 가정해 보죠. 더군다나, 모든 쓰레드가 실행할 코드는 동일해야 합니다. 이런 경우에, 리스트의 시작을 나타내는 전역 포인터는 각 쓰레드에서 서로 다른 위치를 가르키고 있어야 합니다.

이런 포인터를 가지려면 메모리상의 위치가 다른 동일한 전역 변수가 있어야 합니다. 이것이 바로 쓰레드만의 데이타(thread-specific data) 메카니즘이 필요한 이유입니다.


쓰레드만의 데이타 지원 개요(Overview Of Thread-Specific Data Support)

쓰레드만의 데이타(TSD) 메카니즘에서는 키와 값이라는 개념이 필요합니다. 각 키는 이름을 갖고 있고 어떤 메모리 영역을 가르킵니다. 두 개의 서로 다른 쓰레드에서 이름이 같은 키를 갖고 있다면 항상 서로 다른 메모리 위치를 나타냅니다. 이 키를 가지고 접근할 수 있는 메모리 블럭을 할당해 주는 라이브러리 함수들이 이것을 처리해 줍니다. 키를 만들어 주는 함수(전체 프로세스에서 한 키에 대해서 한 번만 실행), 메모리를 할당해 주는 함수(각 쓰레드에서 실행), 특정 쓰레드에서 이 메모리를 다시 반환해 주는 함수, 전체 프로세스에서 그 키를 없애주는 함수등이 있습니다. 또, 키가 가르키는 데이타에 접근하는 함수와 그 값을 세팅하거나 값을 알아내는 함수도 있습니다.


쓰레드만의 데이타 블럭 할당하기(Allocating Thread-Specific Data Block)

pthread_key_create() 함수는 새로운 키를 만들어 내려고 할 때 쓰입니다. 이 키는 전체 프로세스의 모든 쓰레드에서 유효합니다. 키가 생성 됐을 때, 기본으로 NULL을 가르키게 됩니다. 다음에 각 쓰레드들은 자신이 원하는 값으로 이 복사본을 변경하게 됩니다. 사용법을 보여드리죠.

/* rc 는 pthread 함수의 리턴값을 저장하는데 쓰입니다. */int rc;/* 키를 갖고 있을 변수 정의. */pthread_key_t list_key;/* cleanup_list 는 데이타를 청소해 주는 함수입니다. *//* 이것은 우리 프로그램에서 만들어 주는 것이지 TSD 자체의 것이 아닙니다. */extern void* cleanup_list(void*);/* 삭제시 불릴 함수를 넘겨서 키를 만듭니다. */rc = pthread_key_create(&list_key, cleanup_list);

몇 가지 주의사항:

  1. pthread_key_create() 가 리턴한 후에는 'list_key' 변수는 새롭게 생성된 키를 가르키게 됩니다.
  2. pthread_key_create()의 두번째 인자로 넘겨진 함수 포인터는 쓰레드 종료시, pthread 라이브러리에 의해서 키 값의 포인터를 인자로 받아서 불리게 됩니다. 함수 포인터에 NULL 포인터를 넘길 수도 있는데 이렇게 하면 종료시 해당 키에 대해서는 아무 함수도 실행 되지 않습니다. 주의할 점은, 이 키가 한 쓰레드에서 한 번만 생성됐다고 하더라도, 각 쓰레드가 종료할 때마다 실행된다는 것입니다.
    만약에 키를 여러개 생성했다면 키 생성 순서와는 상관없이 해당 종료 함수가 실행 될 것입니다.
  3. pthread_key_create() 함수는 성공시 0을, 실패시 에러 코드를 리턴합니다.
  4. PTHREAD_KEYS_MAX 만큼의 키 값 제한이 있습니다. PTHREAD_KEYS_MAX 가 넘어가게 되면 pthread_key_create() 함수에서 EAGAIN 에러 값을 받게 될 것입니다.


쓰레드만의 데이타에 접근하기(Accessing Thread-Specific Data)

키를 생성한 다음에는 pthread 함수를 써서 접근할 수 있습니다: pthread_getspecific()pthread_setspecific(). 첫번째 함수는 주어진 키에 대해서 그 값을 알아내는 데 쓰이고, 두번째 함수는 주어진 키에 데이타를 세트하는데 쓰입니다. 키 값은 간단하게 void 포인터(void *)이기 때문에, 아무것이나 저장할 수 있습니다. 사용법을 살펴보도록 하죠. 'a_key'는 pthread_key_t 타입으로서, 이미 적당히 초기화된 키 변수라고 가정합니다.


/* 이 변수는 pthread 함수의 리턴 코드값을 저장하는데 쓰입니다. */int rc;/* 데이타를 저장할 변수를 정의합니다. 여기서는 integer 라고 하죠. */int* p_num = (int*)malloc(sizeof(int));if (!p_num) { fprintf(stderr, "malloc: out of memoryn"; exit(1);}/* 변수를 아무 값으로 초기화 합니다. */(*p_num) = 4;/* 이제 이 값을 TSD 키에 저장합니다. *//* 주의할 것은 'p_num' 을 저장하는게 아니라 *//* p_num이 가르키는 값을 저장한다는 것입니다. */rc = pthread_setspecific(a_key, (void*)p_num);../* 어쩌구 저쩌구... */../* 'a_key' 키의 값을 얻어서 출력. */{ int* p_keyval = (int*)pthread_getspecific(a_key); if (p_keyval != NULL) { printf("value of 'a_key' is: %dn", *p_keyval); }}

한 쓰레드에서 키 값을 세트한 후, 다른 쓰레드에서 그 값을 읽어보시기 바랍니다. NULL 을 얻게 될텐데 이 키 값은 쓰레드마다 서로 다르기 때문에 그렇습니다.

pthread_getspecific() 이 NULL을 리턴하는 두 가지 경우를 알아보겠습니다:

  1. 주어진 키가 유효하지 않다(즉, 키가 생성이 안 됐다).
  2. 키 값이 NULL이다. 이는 초기화가 안 됐거나 그 전에 pthread_setspecific()에 의해서 강제로 NULL로 세트됐을 경우중 하나이다.


쓰레드만의 데이타 블럭을 지우기(Deleting Thread-Specific Data Block)

pthread_key_delete() 함수는 키를 지울 때 쓰입니다만, 함수 이름 때문에 헷갈리지 말아야 할 것이 하나 있습니다. 이 함수는 해당 키가 갖고 있는 메모리를 지우지도 않고, 키 생성시 등록된 청소 함수를 부르지도 않습니다. 그러므로, 실행중에 이 메모리를 프리시켜야 한다면 직접 해 줘야 합니다. 하지만 보통, 전역 변수(쓰레드만의 데이타 역시)를 사용한다는 것은 쓰레드가 종료할 때까지 프리시킬 필요가 없을 테고, 이럴 경우에는 쓰레드 라이브러리가 종료함수를 불러줄 것입니다.

이 함수 사용법은 간단합니다. list_key를 알맞게 생성된 키를 가르키는 pthread_key_t 변수라고 가정하면 이런식으로 쓰면 됩니다:

int rc = pthread_key_delete(key);

성공시에는 0을 리턴하고, 주어진 변수가 유효한 TSD 키를 가르키지 않을 경우에는 EINVAL을 리턴합니다.


완전한 예제(A Complete Example)

아직 없습니다. 생각할 시간이 좀 필요하네요. 죄송합니다. 지금 당장 제가 생각할 수 있는 것은 '전역 변수는 아주 나쁘다'라는 것입니다. 앞으로 좋은 예제를 찾아보도록 하겠습니다. 혹시 좋은 예제가 있다면 제게 알려주시기 바랍니다.


쓰레드 취소와 끝내기

쓰레드를 만들었으니 끝내는것도 생각해 볼까요? 몇 가지를 살펴보죠. 쓰레드를 깨끗하게 끝낼 수 있어야 하겠죠. 그리고 아주 고약한 방법인 시그널을 사용하는 프로세스 포크(fork)와는 달리 pthread 라이브러리는 좀 더 신중하게 디자인 돼서 쓰레드를 취소한다든지 끝난 다음의 청소 작업등에 대한 완전한 시스템을 제공합니다. 한 번 살펴보죠.


Canceling A Thread

쓰레드를 끝내려고 할 때는 pthread_cancel를 쓰면 됩니다. 이 함수는 쓰레드 ID를 파라미터로 받아 그 쓰레드 ID로 취소 요청을 보냅니다. 이 요청에 대해 그 쓰레드가 어떻게 할 지는 그 쓰레드의 상태에 달려 있습니다. 즉시 취소될 수도 있고, 취소 위치(뒤에서 설명합니다)에 다다랐을때 취소될 수도 있고, 아예 무시해 버릴 수도 있습니다. 어떻게 쓰레드의 상태를 설정하며 취소 요청에 대해 어떻게 동작하는지에 대한 설정등에 대해서는 뒤에서 살펴보도록 하죠. 일단은 취소 함수를 어떻게 쓰는지 보겠습니다. 'thr_id'는 돌고 있는 쓰레드의 pthread_id 를 갖고 있는 변수라고 합시다.

pthread_cancel(thr_id);

pthread_cancel()은 0을 리턴하기 때문에 성공여부를 알 수가