マルチスレッドでインターバルタイマ

マルチスレッドのプログラムで、スレッドを起動して特定の処理を定期的に実行したいケースはよくあると思う。
時間的な精度を求めない場合はsleepさせれば良いが、ある程度の精度が欲しい場合はインターバルタイマを使用すると思う。 一般的なSIGALRMを使用した割込みでは、待ち受けスレッドが複数ある場合はタイマーが共有されてしまうため、使用できない。
POSIX timerを使用して、スレッドごとに別のシグナルを割り当てることで実現する事は可能であるが、同じ処理をするスレッドを複数作る場合などは、同じスレッド関数を使いまわしたいから、thread IDなどでどのスレッドか識別する処理が必要となり、煩雑になる。
スレッドごとのタイマーが欲しいだけなのに、このような実装をするのは無駄に思える。
ネットでは良い方法が見つからなかったので、調べてみた。

ファイルディスクリプタを用いて通知を行うtimerfd

signalを用いるのではなく、ファイルディスクリプタを代わりに用いてタイマー満了の通知を行うtimerfdを使用する事で、シグナルハンドラやシグナルのマスクなどの煩雑な作業から解放される。
timerfd_createで作ったファイルディスクリプタをreadすると、それ以降にタイマーが満了するまでブロックされるので、sigwaitの代わりにreadを使う事で、定期的に処理を発生させることが可能となります。
timer object自体はスレッドごとに管理されているのか否かがmanには明記されていませんが、ファイルディスクリプタに紐づけられているという事はスレッド毎に独立だろうということで、試したところうまく動いたので、ご紹介します。

example

2つのスレッドを作り、それぞれ1秒と5秒のインターバルタイマーでprint分を定期実行するようにしています。 スレッド異常終了時のクリーンアップハンドラは省略しています。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/timerfd.h>
#include <pthread.h>
#include <sys/syscall.h>

#define THREAD_MAX 8

pid_t gettid(void) {
    return syscall(SYS_gettid);
}

void *timer_handler(uint8_t *t){
    struct itimerspec val = {*t, 0, *t, 0};
    pid_t tid=gettid();
    int tfd;

    printf("%d: interval=%hhd\n", tid, *t);

    if( (tfd=timerfd_create(CLOCK_REALTIME, TFD_CLOEXEC)) < 0){
        perror("timerfd_create");
        exit(EXIT_FAILURE);
    }
    if(timerfd_settime(tfd, 0, &val, NULL) < 0){
        perror("timerfd_settime");
        exit(EXIT_FAILURE);
    }

    while(1){
        uint64_t times;
        if(read(tfd, &times, sizeof(times)) < 0){
            perror("read");
        }
        printf("%d: Start the work!\n", tid);
        if(times > 1)
            printf("%d: Warning: Could not finish the work within timer period!\n", tid);
    }
    close(tfd);
}

int main(int argc, char* argv[]) {
    pthread_t pt[THREAD_MAX];

    uint8_t time = 1;
    if(pthread_create(&pt[0], NULL, (void *)&timer_handler, &time)){
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    uint8_t time2 = 5;
    if(pthread_create(&pt[1], NULL, (void *)&timer_handler, &time2)){
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    sleep(100);
    return (EXIT_SUCCESS);
}

実行すると、TID=1264のスレッドが5秒に一回実行されているのが確認できます。

> gcc -o timer -lpthread timer.c
> ./timer
1263: interval=1
1264: interval=5
1263: Start the work!
1263: Start the work!
1263: Start the work!
1263: Start the work!
1263: Start the work!
1264: Start the work!
1263: Start the work!
1263: Start the work!
1263: Start the work!
1263: Start the work!
1264: Start the work!