0
1

More than 1 year has passed since last update.

スレッド間通信とスレッド単位タイマーを両立する

Last updated at Posted at 2022-02-12

これってどうやって実装するんだろう?と迷ったのでメモ

やりたいこと

「親スレッドが子スレッドを呼び出したとき、または子スレッドが周期的に処理を実行する」ようなマルチスレッドプログラムを実装したい。(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

実装

sigwait_test.c
#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 ~]#

図示するとこんな感じ:
sigwait_test.jpg

ポイント

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という自覚があれば要注意

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1