2007년 9월 10일 월요일

POSIX thread's condition

이런 상황을 생각해보자. 쓰레드가 있고, 그것은 평상시에 자는 상태(CPU점유율 0%)이다. 그런데 문득 할 일이 생겨서 침 흘리며 자고 있는 쓰레드를 두들겨 깨우고 싶다. 어떻게 하면 좋을까?

volatile bool gWakeUp(false);

void*
_thr(void*)
{
    while (!gWakeUp) usleep(1000*1000);
    // something...
}

int
main(int,char**)
{
    // ... init thread.
    gWakeUp = true;
    return 0;
}
이러면 쓰것나? 뭔가 찜찜허다. gWakeUp이 true로 바뀌더라도 _thr는 최악의 경우 1초 뒤에 반응을 보일 것이다. 아힝~* 싫어~*

그래서 나온 녀석이 Condition이다. 술 먹기 전에 한 번 먹고, 먹은 다음 날 한 번 먹는다는 그것!...은 아니다.
#include <cstdarg>
#include <pthread.h>
#include <iostream>
using namespace std;

pthread_cond_t cond;
pthread_mutex_t mtx;
pthread_attr_t attr;

// Just for console.
pthread_mutex_t console_lock;

void
display(const char* fmt, ...)
{
    pthread_mutex_lock(&console_lock);
    va_list lst;
    va_start(lst, fmt);
    vfprintf(stderr, fmt, lst);
    va_end(lst);
    pthread_mutex_unlock(&console_lock);
}

void*
thr(void* param)
{
    const size_t no((size_t)param);
    display("no. %u: I'm born!\n", no);

    do
    {
        pthread_cond_wait(&cond, &mtx);
        // mtx will be locked.

        //- If you want protect something,
        // write statments here.
        pthread_mutex_unlock(&mtx);
        display("no. %u: Yes, Sir!\n", no);

        // hard work something...
        usleep((rand()%1000)*1000);
        display("no. %u: I'm sleeping...\n", no);
    } while (true);

    pthread_exit(NULL);
}

int
main(int,char**)
{
    srand((unsigned int)time(NULL));
    pthread_t hdl;

    // Initialize mutex & condition.
    pthread_mutex_init(&mtx, NULL);
    pthread_cond_init(&cond, NULL);

    // Console mutex.
    pthread_mutex_init(&console_lock, NULL);

    // Thread attributes.
    pthread_attr_init(&attr);
    pthread_attr_setscope(&attr, PTHREAD_SCOPE_PROCESS);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // Create n threads!
    for ( size_t i(0); i<3; i++ )
    {
        pthread_create(&hdl, &attr, thr, (void*)i);
    }

    // Signal condition.
    for ( size_t i(0); i<10; i++ )
    {
        display("Commander: Wake up, Thread!\n");
        pthread_cond_signal(&cond);
        usleep((rand()%1000)*1000);
    }

    // Broadcast signal condition.
    display("Commander: Wake up all!\n");
    pthread_cond_broadcast(&cond);

    usleep(1000000*10);
    return 0;
}
자자, 겁먹지 말자.
대충 보면, 쓰레드를 n개(여기서는 3개) 만들고, 컨디션 하나, 뮤텍스 하나 만든다. (나머진 생략) thr은 signal이 올 때까지 pthread_cond_wait에서 퍼질러 잔다. 이때 main함수에서 pthread_cond_signal을 보내면, 자고 있던 쓰레드가 벌떡! 깨어난다. 깨는 순서는 순전히 운에 달려 있다. 또한 pthread_cond_broadcast로 모든 thr를 깨울 수도 있다.

중요한 것은 condition이 깨는 순간, 파라메터로 넣어준 mutex를 lock하는데, 뭔가 다 끝났으면 잊지 말고 unlock해주자. 이러한 속성을 이용해서 signal/broadcast 앞뒤로 해당 mutex를 lock하고, 보호하고자 하는 것이 있으면 그 사이와 cond_wait~mutex_unlock 사이에 아름답게 써주자.

다음은 위를 컴파일하고 실행한 결과이다.
$ make
g++   -lpthread  cond.cpp   -o cond
$ ./cond
Commander: Wake up, Thread! - 1
no. 0: I'm born!
no. 1: I'm born!
no. 2: I'm born!
Commander: Wake up, Thread!
no. 0: Yes, Sir!
Commander: Wake up, Thread!
no. 1: Yes, Sir!
no. 0: I'm sleeping...
no. 1: I'm sleeping...
Commander: Wake up, Thread!
no. 2: Yes, Sir!
Commander: Wake up, Thread!
no. 0: Yes, Sir!
no. 2: I'm sleeping...
Commander: Wake up, Thread!
no. 1: Yes, Sir!
Commander: Wake up, Thread!
no. 2: Yes, Sir!
Commander: Wake up, Thread!
no. 1: I'm sleeping...
no. 0: I'm sleeping...
Commander: Wake up, Thread!
no. 1: Yes, Sir!
no. 2: I'm sleeping...
Commander: Wake up, Thread!
no. 0: Yes, Sir! - 2
no. 1: I'm sleeping...
Commander: Wake up all! - 3
no. 2: Yes, Sir!
no. 1: Yes, Sir!
no. 0: I'm sleeping... - 4
no. 1: I'm sleeping...
no. 2: I'm sleeping...
실행 결과에서 알 수 있듯이 쓰레드 컨디션에 도달하기도 전에 Signal을 보낸 것(1)은 받은 녀석이 없다. 또한 모두 깨웠는데(3) 모든 쓰레드가 시그널을 받지 못한 까닭은 해당 쓰레드가 cond_wait 상태가 아니었기 때문이다. (2, 4)