これってどうやって実装するんだろう?と迷ったのでメモ
やりたいこと
「親スレッドが子スレッドを呼び出したとき、または子スレッドが周期的に処理を実行する」ようなマルチスレッドプログラムを実装したい。(C言語)
結論
Linux環境下に限定するならば、
- 親スレッド→子スレッドへの呼び出し:pthread_killでシグナル送出
- 子スレッド内でのタイマー:timer_createでsigev_notifyにSIGEV_THREAD_IDを指定して、自スレッドにだけシグナル送出
- 子スレッドはsignal_waitで、親スレッドからの呼び出しシグナルと、タイマー満了シグナルを待機
環境
CentOS Linux release 7.9.2009 (Core)
gcc 4.8.5 20150623 (Red Hat 4.8.5-44)
glibc.x86_64 2.17-317.el7
実装
#include <pthread.h>
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/syscall.h>
// 終了フラグ(1=子スレッド終了)
// プロセス終了時、子スレッドを先に停止するために使用する
int endProcFlag = 0;
// 子スレッド間共通なので、mainで初期化
sigset_t set; // ブロッキング対象のシグナルを示すシグナル集合体
struct itimerspec itval; // タイマーのインターバル
/* マイクロ秒まで標準出力に表示 */
static void printDateTime()
{
struct timeval myTime;
struct tm *time_st;
gettimeofday(&myTime, NULL);
time_st = localtime(&myTime.tv_sec);
printf("[%02d:%02d:%02d.%06d] ", time_st->tm_hour, time_st->tm_min, time_st->tm_sec, myTime.tv_usec);
}
/* 子スレッド:5秒周期でアラーム、または親スレッドからの割り込みで処理実行 */
static void * thread_func()
{
int sig;
timer_t tid;
struct sigevent se;
// タイマー満了時の動作設定
se.sigev_notify = SIGEV_THREAD_ID;
se.sigev_signo = SIGALRM;
// se.sigev_notify_thread_id = gettid(); ★ この記述だとコンパイルエラー(後述)
// se._sigev_un._tid = gettid(); ★ 環境によっては、この記述だとコンパイルエラー(後述)
se._sigev_un._tid = syscall(SYS_gettid);
timer_create(CLOCK_REALTIME, &se, &tid);
timer_settime(tid, 0, &itval, NULL);
for (;;)
{
sigwait(&set, &sig);
// 終了フラグが立っていたら、スレッドを終了する
if (endProcFlag == 1)
{
printDateTime();
printf("Detect end flag. (thread id = 0x%lx)\n", pthread_self());
pthread_exit(NULL);
}
printDateTime();
printf("Signal %d received. (thread id = 0x%lx)\n", sig, pthread_self());
// ここで、SIGUSR1またはタイマー経過時に動かしたい処理
// 最後の処理起動からタイマー時間後(この例ならば5秒後)に立ち上げる
timer_settime(tid, 0, &itval, NULL);
}
}
/* 親スレッド:任意のタイミングで子スレッドにSIGUSR1を配送する */
int main()
{
pthread_t thread1, thread2;
time_t timeval;
int sig;
// タイマーのインターバル設定(初回:5秒後にアラーム、2回目以降:5秒経過後にアラーム)
itval.it_value.tv_sec = 5;
itval.it_value.tv_nsec = 0;
itval.it_interval.tv_sec = 5;
itval.it_interval.tv_nsec = 0;
// SIGUSR1、SIGALRMをブロッキング
sigemptyset(&set);
sigaddset(&set, SIGALRM);
sigaddset(&set, SIGUSR1);
pthread_sigmask(SIG_BLOCK, &set, NULL);
pthread_create(&thread1, NULL, &thread_func, NULL);
printDateTime();
printf("Thread 1 (0x%lx) start.\n", thread1);
sleep(7);
pthread_kill(thread1, SIGUSR1);
pthread_create(&thread2, NULL, &thread_func, NULL);
printDateTime();
printf("Thread 2 (0x%lx) start.\n", thread2);
sleep(8);
pthread_kill(thread2, SIGUSR1);
sleep(8);
// 終了処理:フラグを立てて即時シグナル送信
endProcFlag = 1;
pthread_kill(thread1, SIGUSR1);
pthread_kill(thread2, SIGUSR1);
sleep(1);
return 0;
}
実行結果
※シグナル番号10=SIGUSR1、14=SIGALRM
[root@localhost ~]# ./a.out
[12:21:01.322821] Thread 1 (0x7fcc061c4700) start.
[12:21:06.323435] Signal 14 received. (thread id = 0x7fcc061c4700)
[12:21:08.323347] Thread 2 (0x7fcc059c3700) start.
[12:21:08.323436] Signal 10 received. (thread id = 0x7fcc061c4700)
[12:21:13.323635] Signal 14 received. (thread id = 0x7fcc061c4700)
[12:21:13.323815] Signal 14 received. (thread id = 0x7fcc059c3700)
[12:21:16.323664] Signal 10 received. (thread id = 0x7fcc059c3700)
[12:21:18.323921] Signal 14 received. (thread id = 0x7fcc061c4700)
[12:21:21.323857] Signal 14 received. (thread id = 0x7fcc059c3700)
[12:21:23.324125] Signal 14 received. (thread id = 0x7fcc061c4700)
[12:21:24.323844] Detect end flag. (thread id = 0x7fcc061c4700)
[12:21:24.324352] Detect end flag. (thread id = 0x7fcc059c3700)
[root@localhost ~]#
ポイント
SIGEV_THREAD_ID
timer_createの第二引数に渡すsigevent構造体には、イベント通知方法(この場合、タイマー時間が経過したことの通知方法)として以下のものが選べます。
(参照:man page of sigevent)
- SIGEV_NONE
何もしない - SIGEV_SIGNAL
プロセスにシグナルを送信する (timer_createで第二引数にNULLを渡したときの初期値) - SIGEV_THREAD
新しいスレッドを立ち上げて関数を開始する - SIGEV_THREAD_ID(Linux固有)
指定されたスレッドにシグナルを送信する
移植性を無視できるならば、SIGEV_THREAD_IDを使うことで、timer_settimeのアラームを自分自身のスレッドにのみ飛ばすことができそうです。
スレッドIDの指定
シグナルを送りたい先のスレッドIDは、sigevent構造体の「sigev_notify_thread_id」に設定せよと書かれています。
そこで、
se.sigev_notify_thread_id = gettid();
と記述すると、「sigevent構造体にsigev_notify_thread_idというメンバーはない」と怒られます。
同じような悩みを持つ人がいました。
ここからリンクされているsiginfo.hはリンク切れになっていますが、github上のsiginfo.hを見ると、
確かにman page of sigeventに書かれたsigevent構造体とは違うのがわかります。
ということで、
se._sigev_un._tid = gettid();
に直します。
自スレッドのスレッドIDを取得
man page of timer_createには、「sigev_notify_thread_idには、カーネルのスレッドID(すなわち、clone()やgettid()の戻り値)を指定する」と書いています。
が、
se._sigev_un._tid = gettid();
と書いてもコンパイルエラーになります。
man page of gettidを読むと、
The gettid() system call first appeared on Linux in kernel 2.4.11.
Library support was added in glibc 2.30. (Earlier glibc versions did not provide a wrapper for this system call, necessitating the use of syscall(2).)
ということで、glibc 2.30未満の場合は、syscall経由で呼び出す必要があるとのことです。
そのため、
se._sigev_un._tid = syscall(SYS_gettid);
という記述になっています。
※glibc 2.30のリリースが2019/8/1みたいなので、少しでも古いOSという自覚があれば要注意