pthread_cancel(3)を使ってみた
マルチスレッドのプログラミングをしていく時にあからさまなポーリングをしない方法についてです。後々同じ内容で悩まないためのメモです。
あからさまなポーリングとは
int death_flag; /* 省略 */ void *thread_main(void *arg) { /* 省略 */ while(1){ /* 省略 */ if(death_flag == 1){ break; } } /* 省略 */ }
のようなコードです。このコードはdeath_flagの用に単純な変数1つなら上手くいくものの、ビジーループなので、リソースを消費します。さらにリソースの消費を軽くしようとsleep(3)をはさむと今度はタイミングの問題が出てきます、そしてさらにdeath_flagに相当するものが構造的なものになるとバグが入り込む余地が複合的にうまれ、デバッグが困難になる可能性があります。
ということで、あからさまなポーリングをしないでとは言いつつもできるだけシンプルな方法でmainスレッドから子スレッドを終了させる方法を考えてみます。
そもそも、あるスレッドが他のスレッドを(あからさまなポーリングをしないで)終了させる方法として
- 条件変数とミューテックスを使う
- 子スレッドがシグナルを受信する
- スレッドをキャンセルする
の方法があると思います。考えられる方法を紳士的な順に並べてみました。紳士的な方法はそれ故に制限も多いです。それぞれの方法について詳しく考えていきます。
1番目の条件変数とミューテックスを使用する方法は、そもそもpthread_cond_wait()まで到達させられないので、無限ループを含むような子スレッドがある場合だと使えません。
2番目の方法は子スレッドがシグナルを受信するという方法です。シグナルハンドラ内ではシグナルセーフな関数以外は呼ぶべきでないということになっている*1ことと、プロセス全体に対するシグナルとの住み分けをきちんと決めておかないといけないという留意点があります。
3番目の方法はmainスレッドが好きな時に子スレッドを強制的に終了させるという、なりふり構わない方法です。管理する側としては使いやすいものの、この方法だとキャンセルポイントを把握しないといけないということ、C++とは互換性が無いコードになるという制限があります*2。
今回はmainスレッドと子スレッド1つを持ち、mainスレッドは内部で無限ループし、SIGTERMシグナルを受信して終了するというシステムを前提として考えます。mainスレッドだけがシグナルを受信するという設計でいきたいので、mainスレッド以外でのシグナル処理はできるだけ避けたいという意味で2番目は却下。3番目の方法を考えてみます。
環境:Linux debian 2.6.39-2-amd64 [debian wheezy]
という訳で、3番目の方法でマルチスレッドのプログラムを書いてみました。まずはmainスレッドのコードです。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <signal.h> /* pthread_cancel()とpthread_cleannup_push()、pthread_cleanup_pop()のテスト gcc -c pthread_cancel.c gcc -c pthread_cancel.worker.c gcc -o pthread_cancel pthread_cancel.o pthread_cancel.worker.o -lpthread ./pthread_cancel */ void *worker_sample(void *arg); /* pthread_cancel.worker.c */ int main(int argc, char *argv[]) { int signo; sigset_t ss; pthread_t th; /* シグナルハンドリングの準備 */ sigemptyset(&ss); /* block SIGTERM */ if(sigaddset(&ss, SIGTERM) == -1){ goto ret; } sigprocmask(SIG_BLOCK, &ss, NULL); pthread_create(&th, NULL, worker_sample, NULL); for(;;){ if(sigwait(&ss, &signo) == 0){ /* シグナルが受信できたら */ if(signo == SIGTERM){ puts("sigterm recept"); break; } } } pthread_cancel(th); pthread_join(th, NULL); ret: return 0; }
mainスレッドではシグナルを受信するだけのものです。受信できたらsigwaitのブロックから抜けますので、同時にwhileループからも抜けて子スレッドにキャンセル要求をだして、pthread_join(3)で待ちます。
次に子スレッドのコードです。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #define BUFLEN 256 /* scanf(3)でデータを格納するメモリのバッファサイズ */ FILE *f; char *message; void alloc_resources(void) { f = fopen("/dev/null", "w+"); message = calloc(BUFLEN, sizeof(char)); } void free_resources(void *arg) { puts("キャンセルします"); fclose(f); free(message); } void init_worker(void) { int old; pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old); printf("old cancelstate:%d\n", old); pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &old); printf("old canceltype:%d\n", old); } void *worker_sample(void *arg) { alloc_resources(); init_worker(); pthread_cleanup_push(free_resources, NULL); while(1){ scanf("%s", message); fprintf(f, "%s", message); printf("「%s」を書き込みました\n", message); } pthread_exit(NULL); pthread_cleanup_pop(1); }
子スレッド側では標準入力からのデータを受け取って/dev/nullに書き込むという実際のロジックを実装してあります。
mainスレッド側からのキャンセル要求を受け付けるために通常はpthread_testcancel(3)を呼び出すべきなのでしょうが、read(2)やwrite(2)はキャンセルポイントになっているのでpthread_testcancel(3)は呼び出していません。
さらに一応init_worker()でキャンセルステートとキャンセルタイプを再設定しています。
実行結果です。
pthread_cleanup_push(3)で登録した関数は実行中のスレッドのコンテキストで実行されていることが確認できます(マクロなので当然ですが)。さらに、実行した環境ではキャンセルステートとキャンセルタイプはデフォルトで「キャンセル許可」、「遅延キャンセル」になっているようです。
/usr/include/pthread.h
/* Cancellation */ enum { PTHREAD_CANCEL_ENABLE, #define PTHREAD_CANCEL_ENABLE PTHREAD_CANCEL_ENABLE PTHREAD_CANCEL_DISABLE #define PTHREAD_CANCEL_DISABLE PTHREAD_CANCEL_DISABLE }; enum { PTHREAD_CANCEL_DEFERRED, #define PTHREAD_CANCEL_DEFERRED PTHREAD_CANCEL_DEFERRED PTHREAD_CANCEL_ASYNCHRONOUS #define PTHREAD_CANCEL_ASYNCHRONOUS PTHREAD_CANCEL_ASYNCHRONOUS };
pthreadは多くのAPIが用意されていて、自分のシステムに最適なスレッドの制御方法を選択するのが難しいです。ただ、スレッドの制御はサーバやマルチスレッドアプリを書く時には必ず遭遇する課題ですし、バグも発生しやすいので、シンプルで堅実な方法を選択するのがいいような気がします。
*1:http://d.hatena.ne.jp/yupo5656/20040712/p2
*2:http://udrepper.livejournal.com/21541.html