Linux Programming - 스레드(Thread)의 종료

스레드는 어떻게 종료되며 또 어떻게 종료시킬 수 있는지 알아보겠습니다.

스레드에서 exit, _Exit, _exit를 호출하면 스레드가 속해있는 프로세스가 종료됩니다. default 시그널 액션이 프로세스 종료로 지정되어 있을때도, 하나의 스레드에서 시그널을 받으면 프로세스 자체가 종료되어 버리죠.

프로세스는 종료하지 않고 스레드만 종료시키고자 할 때는 어떻게 해야 할까요? 이를 위한 세 가지 방법이 있습니다.

  • 스레드의 start 루틴에서 리턴
  • 스레드에서 pthread_exit 호출
  • 다른 스레드에서 pthread_cancel 호출

스레드의 start 루틴에서 리턴하는 방법은 단순하니 따로 설명하지는 않겠습니다.
그럼 먼저 pthread_exit를 이용한 스레드 종료에 대해서 살펴보겠습니다. 다음은 pthread_exit의 프로토타입 입니다.
void pthread_exit (void *rval_ptr);
pthread_exit함수는 하나의 argument를 가지는데요. 이 rval_ptr은 pthread_join을 호출하는 다른 스레드에서 받아서 사용할 수 있습니다. pthread_exit를 얘기하다가 갑자기 pthread_join이 나왔군요. 말이 나온김에 pthread_join에 대해서 언급을 하고 넘어가도록 하죠. 먼저, 프로토타입부터 보겠습니다.
int pthread_join (pthread_t thread, void **rval_ptr);
pthread_join을 호출한 스레드는 첫번째 argument에 지정된 스레드가 종료될때까지 블록됩니다. 즉, A 스레드가 B 스레드에 대해 pthread_join을 걸면 A 스레드는 B 스레드가 종료될때까지 멈추어 대기한다는 말입니다. rval_ptr은 지정된 스레드가 종료되면서 채워주는데, 스레드가 종료되는 방법에 따른 rval_ptr 값은 다음과 같습니다.

  • start 루틴에서 리턴한 경우: 리턴 코드가 채워집니다.
  • 다른 스레드에서 pthread_cancel을 호출 한 경우: rval_ptr은 PTHREAD_CANCELED를 가리킵니다.
  • pthread_exit를 호출한 경우: pthread_exit의 argument로 채워집니다.

또한, pthread_join 함수는 해당 스레드를 detatched 상태로 만들어서 스레드에서 사용하던 자원을 회수할 수 있게 해줍니다. 만약 이미 detatch 된 스레드에 대해서 pthread_join을 호출하면 EINVAL 을 리턴하면서 실패됩니다.

스레드의 리턴값을 굳이 알아야 할 필요가 없다면, rval_ptr에 NULL을 사용해도 무방합니다.

다음은 스레드의 종료 코드를 출력하는 sample code 입니다.

void * thr_fn1(void *arg)
{
    printf("thread 1 returning\n");
    return((void *)1);
}

void * thr_fn2(void *arg)
{
    printf("thread 2 exiting\n");
    pthread_exit((void *)2);
}

int main(void)
{
    int err;
    pthread_t tid1, tid2;
    void *tret;

    err = pthread_create(&tid1, NULL, thr_fn1, NULL);
    if (err != 0)
        err_quit("can't create thread 1: %s\n", strerror(err));

    err = pthread_create(&tid2, NULL, thr_fn2, NULL);
    if (err != 0)
        err_quit("can't create thread 2: %s\n", strerror(err));

    err = pthread_join(tid1, &tret);
    if (err != 0)
        err_quit("can't join with thread 1: %s\n", strerror(err));
    printf("thread 1 exit code %d\n", (int)tret);

    err = pthread_join(tid2, &tret);
    if (err != 0)
        err_quit("can't join with thread 2: %s\n", strerror(err));
    printf("thread 2 exit code %d\n", (int)tret);

    exit(0);
}

위 코드를 실행시키면 아래와 같은 결과를 볼 수 있습니다.

$ ./a.out
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2
스레드의 start 루틴에서 리턴을 하거나 pthread_exit를 호출하면, 다른 스레드(여기서는 main 스레드)에서 pthread_join을 이용하여 종료 코드를 전달 받는다는 것을 알 수 있네요.

이번엔, pthread_cancel을 이용해 다른 스레드를 종료시키는 방법을 알아보겠습니다.
int pthread_cancel (pthread_t tid);

pthread_cancel을 호출하면 tid에 해당하는 스레드에서 pthread_exit를 PTHREAD_CANCELED argument를 주고 호출한 것과 동일하게 동작합니다.(기본적으로 이렇습니다만 다르게 동작하도록 할 수도 있습니다) 또한, ptherad_cancel은 pthread_join과는 달리 스레드가 종료될때까지 블록되지 않습니다. 단지, 스레드를 종료하라는 요청만 던지고 다음 코드로 진행합니다.


atexit를 이용하여 프로세스가 종료될때 호출되는 유저 function을 지정하는 것처럼, 스레드가 종료될때 호출될 유저 function을 지정할 수 있습니다. 이 유저 function을 <thread cleanup handler>라고 하는데, 이것을 통해서 스레드에서 사용하던 자원을 반납하거나 데이터를 정리할 수 있습니다. <thread cleanup handler>를 등록하면 스택(LIFO)의 형태로 저장됩니다. 따라서, 스레드가 종료될때 <thread cleanup handler>는 등록한 순서의 역순으로 호출됩니다. <thread cleanup handler>를 등록하고 호출하기 위해서 다음 함수를 사용할 수 있습니다.

void pthread_cleanup_push (void (*rtn)(void *), void *arg);
void pthread_cleanup_pop (int execute);

pthread_cleanup_push 함수는 arg를 argument로 갖는 rtn 함수를 <thread cleanup handler>로 등록합니다. 이렇게 등록된 <thread cleanup handler>는 다음 3가지 경우에 실행됩니다.

  • 스레드 안에서 pthread_exit가 호출된 경우
  • 다른 스레드에서 pthread_cancel를 호출한 경우
  • 스레드 안에서 pthread_cleanup_pop함수가 호출된 경우(단, execute가 0이 아닐때)

만약, pthread_cleanup_pop함수의 argument가 0인 상태로 호출되면, 그때마다 cleanup handler를 차례(등록의 역순으로)로 제거합니다.

위의 두 함수의 사용시에 한 가지 주의할 점이 있습니다. pthread_cleanup_push함수와 pthread_cleanup_pop함수는 반드시 같은 scope 내에서 짝을 맞추어 사용해야 합니다. 실제로 이 두 함수는 매크로로 구현되어 있는데, pthread_cleanup_push 함수 내에서 브레이스(중괄호)를 열었다가, pthread_cleanup_pop 함수에서 이것을 닫는 과감한 구성이 있기때문입니다. 그럼 sample code를 보면서 이 함수들을 어떻게 사용하는지 알아보겠습니다.

void cleanup(void *arg)
{
    printf("cleanup: %s\n", (char *)arg);
}

void * thr_fn1(void *arg)
{
    printf("thread 1 start\n");
    pthread_cleanup_push(cleanup, "thread 1 first handler");
    pthread_cleanup_push(cleanup, "thread 1 second handler");
    printf("thread 1 push complete\n");
    if (arg)
        return((void *)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return((void *)1);
}

void * thr_fn2(void *arg)
{
    printf("thread 2 start\n");
    pthread_cleanup_push(cleanup, "thread 2 first handler");
    pthread_cleanup_push(cleanup, "thread 2 second handler");
    printf("thread 2 push complete\n");
    if (arg)
        pthread_exit((void *)2);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_exit((void *)2);
}
 
int main(void)
{
    int err;
    pthread_t tid1, tid2;
    void *tret;

    err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
    if (err != 0)
        err_quit("can't create thread 1: %s\n", strerror(err));

    err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
    if (err != 0)
        err_quit("can't create thread 2: %s\n", strerror(err));

    err = pthread_join(tid1, &tret);
    if (err != 0)
        err_quit("can't join with thread 1: %s\n", strerror(err));
    printf("thread 1 exit code %d\n", (int)tret);

    err = pthread_join(tid2, &tret);
    if (err != 0)
        err_quit("can't join with thread 2: %s\n", strerror(err));
    printf("thread 2 exit code %d\n", (int)tret);

    exit(0);
}

위 코드를 실행하면 다음과 같은 결과를 볼 수 있습니다.

$ ./a.out
thread 1 start
thread 1 push complete
thread 2 start
thread 2 push complete
cleanup: thread 2 second handler
cleanup: thread 2 first handler
thread 1 exit code 1
thread 2 exit code 2

thread 1은 단순하게 return 하였으므로 <thread cleanup handler>가 동작하지 않은 반면, thread 2는 pthread_exit로 종료했기에 <thread cleanup handler>가 등록의 역순으로 동작한 것을 알 수 있습니다.


기본적으로 스레드의 종료 코드는 스레드가 종료되더라도 pthread_join이 호출될 때까지 유지됩니다. 하지만, detatch 상태의 스레드는 종료되는 즉시 자원이 회수됩니다. 또한, detatch 된 스레드에 대해서는 pthread_join으로 스레드 종료 상태를 기다릴 수 없습니다.(EINVAL을 리턴하면서 실패합니다) 다음의 pthread_detatch 함수를 사용하면 스레드를 detatch 상태로 변경할 수 있습니다.

int pthread_detach (pthread_t tid);

참고로, 스레드를 생성할때 attribute를 이용하여 detatch 상태인 스레드를 만들 수도 있습니다.


참고문서
[1] Advanced Programming in the Unix Environment - 2nd Edition - W.R.Stevens

0 개의 댓글:

댓글 쓰기