pthread ライブラリを使い、スレッド間の同期・通信を行うために pthread_cond_signal と
pthread_cond_wait を使うことがあります。
また、永遠にシグナルを待ち続けることが無いように、pthread_cond_timedwait を使って
一定時間シグナルが届かなかった場合にタイムアウトさせるAPIも用意されています。
しかし、この pthread_cond_timedwait は使い方を誤ると意図した動作をしなくなります。
つまりタイムアウトしなくなります。(関数呼び出しから戻ってこなくなる)
本記事では、pthread_cond_timedwait の仕様、どのような条件が揃うとタイムアウトしなくなるのか、
また、それを防ぐ実装方法を紹介いたします。
関数仕様
書式
|
1 2 3 4 5 6 |
#include <pthread.h> int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); |
引数
| cond | pthread_cond_signal or pthread_cond_broadcast で送信されるシグナルを待つ条件変数 |
| mutex | 条件変数を保護するための mutex ロック変数 |
| abstime | シグナルを待つ時間を指定する(絶対時間) |
戻り値
| 0 | シグナルを受信し、正常に処理を完了 |
| EINVAL | 引数 cond, mutex または abstime によって指定された値が無効 |
| EINTR | 非同期シグナルによって割り込まれた |
| ETIMEDOUT | システム時間が abstime で指定した時間に到達、または超過 |
機能
- 条件変数 cond にシグナルが到達するまでスレッドをブロック
mutex のアンロックと条件変数 cond へのシグナル待ちを同時に行います。
pthread_cond_signal または pthread_cond_broadcast で条件変数にシグナルが送信されるまで、
スレッドの実行は停止され、CPUを消費しません。関数から戻る際に、再び mutex をロックします。 - abstime で待ち時間の設定
abstime で指定された時間内にシグナルが送信されなかった場合、mutex を再びロックし、
ETIMEDOUT を返します。デフォルトでは、1970/1/1 00:00:00 を起点とする絶対時刻で指定します。
タイムアウトしないメカニズム
例えば、以下のような場合を考えてみます。

pthread_cond_timedwaitがタイムアウトしないメカニズム
CLOCK_REALTIME で取得できる時刻や、gettimeofday() で取得できる「絶対時刻」は、
1970/1/1 00:00:00 を起点とした時刻で、ファイルのタイムスタンプなどに使用されています。
この時刻は、NTP(Network Time Protocol)などによって補正されます。
例えば、上図のようなロジックが実行された場合、以下のように過去を指定して待ちに入ってしまいます。
過去を指定して待ちに入ると、永遠にタイムアウトしなくなります。
- clock_gettime() で、2022/03/29 00:00:00 を取得する
- 2秒のあいだ条件変数 cond へのシグナル送信を待ち受けるため、tv_sec に 2 を加算
⇒つまり、2022/03/29 00:00:02 まで待つ - pthread_cond_timedwait を呼び出すまでに、NTP により 2022/03/29 00:00:05 に時刻補正
- 補正の結果、過去の時刻を指定して pthread_cond_timedwait を呼び出す
タイムアウトしない現象を防ぐ方法
では、どのようにして防げばよいのでしょうか?
Unix 系の OS は、絶対時刻の他にも様々な時刻情報を持っています。
代表的なものは MONOTONIC と呼ばれる時刻で、カーネルが起動している経過時間を示す時刻です。
dmesg の各行の頭に付いている時刻で、NTP などの時刻補正の影響を受けません。
この MONOTONIC 時刻を使用して、pthread_cond_timedwait の待ち時間を指定します。
これには、pthread_cond_init で条件変数を初期化する際に、pthread_condattr_setclock で
条件変数 cond に時刻の属性を設定する必要があります。
サンプルプログラム
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
#include <errno.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include <unistd.h> typedef struct { pthread_t tid; pthread_mutex_t mtx; pthread_condattr_t cattr; pthread_cond_t cond; } td_arg_t; void *SignalThread(void* arg) { td_arg_t *td = (td_arg_t *)arg; while(1) { pthread_mutex_lock(&td->mtx); pthread_cond_signal(&td->cond); pthread_mutex_unlock(&td->mtx); sleep(2); } pthread_exit(0); return 0; } int main(void) { td_arg_t td_arg; struct timespec ts; int ret; void *res; pthread_mutex_init(&td_arg.mtx, NULL); pthread_condattr_init(&td_arg.cattr); pthread_condattr_setclock(&td_arg.cattr, CLOCK_MONOTONIC); pthread_cond_init(&td_arg.cond, &td_arg.cattr); ret = pthread_create(&td_arg.tid, NULL, SignalThread, &td_arg); if (ret) { perror("pthread_create"); _exit(-1); } while(1) { pthread_mutex_lock(&td_arg.mtx); clock_gettime(CLOCK_MONOTONIC, &ts); ts.tv_sec += 1; ret = pthread_cond_timedwait(&td_arg.cond, &td_arg.mtx, &ts); switch(ret) { case 0: printf("recieved signal.\n"); break; case ETIMEDOUT: printf("timeout.\n"); break; default: printf("error. (ret=%d, errno=%d)\n", ret, errno); break; } pthread_mutex_unlock(&td_arg.mtx); } // Not reached pthread_cancel(td_arg.tid); pthread_join(td_arg.tid, &res); pthread_cond_destroy(&td_arg.cond); pthread_condattr_destroy(&td_arg.cattr); pthread_mutex_destroy(&td_arg.mtx); return 0; } |