C言語におけるタイマー処理は、組み込みシステムの制御から、ゲーム開発、ネットワーク通信のタイムアウト処理まで、幅広い分野で必要とされる技術です。
プログラムを指定した時間だけ停止させる「スリープ」や、処理に要した「経過時間の計測」、さらには「一定間隔でのイベント実行」など、用途に応じて最適な関数を選択しなければなりません。
本記事では、標準ライブラリからPOSIX準拠の高精度な手法まで、C言語におけるタイマー実装の核心部分を分かりやすく解説します。
C言語におけるタイマー処理の重要性
現代のソフトウェア開発において、時間はプログラムの挙動を制御するための重要なリソースの一つです。
単に実行を待機させるだけでなく、システムのパフォーマンスを最適化したり、リアルタイム性を確保したりするために、適切なタイマー実装が求められます。
特にC言語はハードウェアに近い層で動作するため、OSやプロセッサの特性を理解した上での時間制御が不可欠です。
用途に合わない関数を選択してしまうと、不必要なCPUリソースの消費や、意図しない精度の低下を招く恐れがあります。
基本的なプログラムの停止(スリープ処理)
最も直感的なタイマー処理は、プログラムの実行を一時的に停止させるスリープ処理です。
sleep関数による秒単位の停止
unistd.hヘッダで提供されるsleep関数は、指定した秒数だけプロセスを停止させます。
この関数は非常にシンプルですが、秒単位での指定しかできないため、ミリ秒やマイクロ秒といった細かい制御には向きません。
また、シグナルの割り込みが発生した場合、指定した時間よりも早く復帰することがあります。
nanosleep関数による高精度な待機
より精密な待機を行いたい場合には、nanosleep関数を使用するのが現代的な標準です。
nanosleepはナノ秒単位での指定が可能であり、POSIX.1に準拠しているため、UNIX系システムで広く利用されています。
また、struct timespec構造体を利用することで、残り時間を取得して割り込みに対応することも可能です。
#include <stdio.h>
#include <time.h>
int main() {
struct timespec req, rem;
// 1.5秒(1秒 + 500,000,000ナノ秒)を設定
req.tv_sec = 1;
req.tv_nsec = 500000000;
printf("1.5秒間待機します...\n");
// nanosleepの実行
if (nanosleep(&req, &rem) < 0) {
printf("ナノスリープが中断されました。残り: %ld 秒, %ld ナノ秒\n", rem.tv_sec, rem.tv_nsec);
return 1;
}
printf("待機が完了しました。\n");
return 0;
}
1.5秒間待機します...
待機が完了しました。
経過時間の計測方法
アルゴリズムの実行速度を測定したり、一定時間が経過したかを判定したりする場合、経過時間の計測が必要になります。
clock関数によるCPU時間の計測
time.hに含まれるclock関数は、プログラムが開始されてから消費されたCPUクロック数を返します。
これは「壁時計(実時間)」ではなく、CPUがそのプロセスを処理した時間を表します。
マルチスレッド環境では各スレッドの合計時間が返される場合があるため、注意が必要です。
time関数によるカレンダー時刻の取得
time関数は、1970年1月1日からの経過秒数(UNIXエポック)を取得します。
1秒単位での計測には適していますが、高精度な計測には不向きです。
clock_gettime関数による高精度計測
現在の主流は、clock_gettime関数を利用した計測です。
この関数では、システム全体の実時間(CLOCK_REALTIME)や、システム起動時からの経過時間(CLOCK_MONOTONIC)を指定できます。
パフォーマンス計測には、システムの時刻変更に左右されないCLOCK_MONOTONICを使用するのが鉄則です。
#include <stdio.h>
#include <time.h>
#include <unistd.h>
int main() {
struct timespec start, end;
// 計測開始
clock_gettime(CLOCK_MONOTONIC, &start);
// ダミーの処理(1秒スリープ)
sleep(1);
// 計測終了
clock_gettime(CLOCK_MONOTONIC, &end);
// 秒とナノ秒の差分を計算
double elapsed = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) * 1e-9;
printf("経過時間: %.6f 秒\n", elapsed);
return 0;
}
経過時間: 1.000123 秒
一定間隔で処理を実行するタイマー
一定の間隔で特定の関数を実行したい場合、シグナルや非同期処理を利用した実装が必要になります。
alarm関数による単発アラーム
alarm関数は、指定した秒数後に自分自身に対してSIGALRMシグナルを送信します。
非常に軽量ですが、一度きりの実行であるため、繰り返しの場合はシグナルハンドラ内で再度設定する必要があります。
setitimerを用いたインターバルタイマー
setitimerは、マイクロ秒単位でタイマーを設定できる関数です。
ITIMER_REAL、ITIMER_VIRTUAL、ITIMER_PROFの3つのタイマーモードを選択できます。
しかし、現在ではPOSIXタイマーであるtimer_createシリーズへの移行が推奨されています。
POSIXタイマー(timer_create)による高度な制御
より柔軟なタイマーが必要な場合、timer_create、timer_settime関数を使用します。
この手法では、タイマー満了時にシグナルを送るだけでなく、新しいスレッドを作成して関数を実行することも可能です。
これにより、メインループを停止させることなくバックグラウンドでタイマー処理を実行できます。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
void timer_handler(union sigval sv) {
printf("タイマーが発火しました!\n");
}
int main() {
timer_t timerid;
struct sigevent sev;
struct itimerspec its;
// タイマー満了時の通知設定(スレッド作成方式)
sev.sigev_notify = SIGEV_THREAD;
sev.sigev_notify_function = timer_handler;
sev.sigev_value.sival_ptr = &timerid;
sev.sigev_attribute = NULL;
// タイマーの作成
if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) {
perror("timer_create");
exit(1);
}
// 初回発火まで2秒、その後1秒おきに繰り返す設定
its.it_value.tv_sec = 2;
its.it_value.tv_nsec = 0;
its.it_interval.tv_sec = 1;
its.it_interval.tv_nsec = 0;
// タイマーの開始
if (timer_settime(timerid, 0, &its, NULL) == -1) {
perror("timer_settime");
exit(1);
}
printf("タイマーを開始しました。5秒間メインループを動かします。\n");
sleep(5);
// タイマーの削除
timer_delete(timerid);
printf("タイマーを終了しました。\n");
return 0;
}
タイマーを開始しました。5秒間メインループを動かします。
タイマーが発火しました!
タイマーが発火しました!
タイマーが発火しました!
タイマーを終了しました。
タイマー実装の使い分け一覧
プロジェクトの要件に応じて適切な関数を選択できるよう、主な手法を比較表にまとめました。
| 手法・関数名 | 時間単位 | 主な用途 | 特徴 |
|---|---|---|---|
| sleep | 秒 | 単純な待機 | 簡単だが精度は低い |
| nanosleep | ナノ秒 | 高精度な待機 | POSIX準拠、割り込みに対応可能 |
| clock | クロック数 | CPU負荷計測 | CPU使用時間の測定用 |
| clock_gettime | ナノ秒 | 精密な経過時間計測 | モノトニッククロックが利用可能 |
| timer_create | ナノ秒 | 周期的な非同期処理 | 高機能、スレッド連携が可能 |
マルチスレッド環境での注意点
マルチスレッドプログラムでタイマーを扱う場合、いくつかの技術的な課題が生じます。
まず、sleepやnanosleepはそのスレッドのみを停止させますが、プロセス全体に影響を与える可能性のあるシグナルベースのタイマーには注意が必要です。
シグナルはどのスレッドに届くか予測しにくいため、マルチスレッド環境ではSIGEV_THREADを利用したタイマー通知が推奨されます。
また、共有変数へのアクセスにはミューテックスなどの同期機構を適切に使用し、データ競合を防ぐ必要があります。
さらに、高精度タイマーを使用しても、OSのコンテキストスイッチによる遅延(レイテンシ)を完全に排除することはできません。
厳密なリアルタイム性が求められる場合は、リアルタイムOS(RTOS)の検討も必要になります。
Windows環境におけるタイマー処理
ここまでは主にPOSIX準拠の手法を解説してきましたが、Windows環境では異なるAPIが必要になります。
例えば、スリープにはSleep関数(ミリ秒単位)を使用します。
高精度な計測にはQueryPerformanceCounterとQueryPerformanceFrequencyを組み合わせて使用するのが一般的です。
クロスプラットフォームな開発を目指す場合は、これらの差異を吸収するラッパーライブラリを作成するか、標準のtime.hを工夫して使う必要があります。
まとめ
C言語におけるタイマー処理は、単純なsleepから高度なtimer_createまで、用途に合わせて多くの選択肢が存在します。
秒単位の簡単な制御であれば標準関数で十分ですが、精度の高い計測や非同期処理を実現するにはPOSIX拡張機能の理解が不可欠です。
特にclock_gettimeによるモノトニッククロックの利用は、現代的なプログラムにおいて極めて重要です。
本記事で紹介した各種手法を適切に組み合わせることで、堅牢で効率的な時間制御を実現できるでしょう。
タイマーの精度や特性を正しく把握し、開発しているシステムの要件に最も合致した実装を選択してください。
