はじめに (対象読者・この記事でわかること)
本記事は、C 言語でイベント駆動プログラミングを行う開発者、特に libev を使って非同期処理を実装したい方を対象としています。読者は以下のことが理解できるようになります。
- ev_async の基本的な役割と動作原理
- 他スレッドやシグナルハンドラからイベントループへ安全に通知する方法
- 実際のコード例を通じて、ev_async を用いたタスクキューやロギングの実装
本稿を書いた背景は、マルチスレッド環境で libev のイベントループに対し「即時に」通知したいシーンが多く、公式ドキュメントだけでは実装例が少なく、戸惑う開発者が多いことに気付いたためです。実践的なパターンを示すことで、読者の開発効率向上に寄与したいと考えました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- C 言語の基礎(ポインタ、構造体、関数ポインタ)
- libev の基本的な使い方(ev_loop、ev_io、ev_timer 等)
- マルチスレッドプログラミングの概念(pthread)
ev_async の概要と役割
ev_async は libev が提供する 「非同期通知」専用のウォッチャ」 です。通常の I/O やタイマーは OS のイベント(fd の可読性やタイマー満了)に依存しますが、ev_async は プログラム内部から自分自身のイベントループへ手動でシグナルを送る ことができます。主な用途は次の通りです。
-
他スレッドからの通知
複数スレッドが存在する場合、バックグラウンドスレッドが「処理すべき仕事が発生した」ことをメインスレッドのイベントループに伝える手段として利用します。ev_async_send()を呼び出すだけで、メインループに登録されたコールバックが即座に実行されます。 -
シグナルハンドラからの安全な呼び出し
POSIX シグナルハンドラは制限された環境でしか実行できませんが、シグナルハンドラ内でev_async_send()を使えば、シグナル発生をイベントループに転送し、通常のコンテキストで安全に処理できます。 -
状態遷移や内部フラグの更新
イベントループ外部で変更された状態を、次のループサイクルで確実に反映させる場面でも便利です。
仕組みの簡易図
[スレッドA] -- ev_async_send() --> [libev内部] -- コールバック --> [メインスレッド]
ev_async_send() は内部で パイプ (UNIX) もしくはイベントFD を書き込み、libev がそれを読み取ってコールバックをキックします。このため、スレッド間のメモリバリア が自動的に保証され、データ競合のリスクが低減します。
ev_async を使った実装例と注意点
以下では、マルチスレッドでキューにタスクを投入し、メインループで順次処理する 典型的なパターンをステップごとに解説します。実装は POSIX スレッド (pthread) と libev の ev_async、ev_io、ev_timer を組み合わせたものです。
ステップ1:基本的なセットアップ
まずは libev のイベントループと ev_async の初期化を行います。
C#include <ev.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> static struct ev_loop *loop; static ev_async async_watcher; /* メインループ側で実行されるコールバック */ static void async_cb(EV_P_ ev_async *w, int revents) { (void)revents; printf("[main] async event received, processing queued tasks...\n"); /* キューからタスクを取り出して実行するロジックをここに */ }
ev_async_init(&async_watcher, async_cb);でコールバックを登録ev_async_start(loop, &async_watcher);でループに追加
ステップ2:バックグラウンドスレッドから通知
次に、タスクを格納する簡易キューと、バックグラウンドスレッドの例を示します。
C/* タスク構造体 */ typedef struct task { void (*fn)(void *); void *arg; struct task *next; } task_t; /* グローバルキュー(単純なシングルリンクリスト) */ static task_t *queue_head = NULL; static task_t *queue_tail = NULL; static pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER; /* キューにタスクを追加する関数 */ static void enqueue_task(task_t *t) { pthread_mutex_lock(&queue_mutex); t->next = NULL; if (queue_tail) { queue_tail->next = t; } else { queue_head = t; } queue_tail = t; pthread_mutex_unlock(&queue_mutex); } /* ワーカースレッド本体 */ static void *worker_thread(void *arg) { (void)arg; for (int i = 0; i < 5; ++i) { task_t *t = malloc(sizeof(task_t)); t->fn = (void (*)(void *))printf; t->arg = (void *)"Task executed from worker!\n"; enqueue_task(t); /* 非同期通知 */ ev_async_send(loop, &async_watcher); sleep(1); // 疑似的な処理時間 } return NULL; }
ポイント:
ev_async_send(loop, &async_watcher);は スレッドセーフ です。内部でメモリバリアが確保されるので、enqueue_task完了後に即座に通知できます。- キュー操作は
pthread_mutexで保護していますが、ev_asyncのみで排他制御はできません。キューの整合性は自前で確保してください。
ステップ3:メインループ側でタスクを取り出す
async_cb 内でキューからタスクを取り出し、実行します。
Cstatic void async_cb(EV_P_ ev_async *w, int revents) { (void)revents; (void)w; // 未使用警告回避 printf("[main] async event received.\n"); while (1) { pthread_mutex_lock(&queue_mutex); if (!queue_head) { pthread_mutex_unlock(&queue_mutex); break; // キューが空になったら終了 } task_t *t = queue_head; queue_head = t->next; if (!queue_head) queue_tail = NULL; pthread_mutex_unlock(&queue_mutex); /* タスク実行 */ t->fn(t->arg); free(t); } }
- キューが空になるまでループし、まとめて処理 することで
ev_asyncが複数回送信された場合でもロスなく処理できます。 printfだけでなく、任意の関数ポインタを格納できる汎用タスク構造にしている点が重要です。
ステップ4:全体の起動コード
Cint main(void) { loop = EV_DEFAULT; ev_async_init(&async_watcher, async_cb); ev_async_start(loop, &async_watcher); pthread_t tid; if (pthread_create(&tid, NULL, worker_thread, NULL) != 0) { perror("pthread_create"); return 1; } /* メインループ開始 */ ev_run(loop, 0); pthread_join(tid, NULL); return 0; }
実行すると、バックグラウンドスレッドが 1 秒ごとにタスクをキューに渡し、ev_async がメインループに通知します。メインスレッドは即座に async_cb が呼び出され、キューからタスクを取り出して実行します。
ハマった点やエラー解決
| 発生した問題 | 原因 | 解決策 |
|---|---|---|
ev_async_send() が呼び出された後にコールバックが呼ばれない |
ev_async が 開始されていなかった。ev_async_start() を忘れていた。 |
ev_async_init 後に必ず ev_async_start を実行。 |
| キューからタスクを取り出すときに デッドロック が発生 | async_cb 内で pthread_mutex_lock → ev_async_send → 同じロックを取得しようとした。 |
通知はキューにタスクを入れた直後に行い、コールバック側ではロック取得だけに留める。 |
| プログラムが終了しない | メインループが ev_break で止められなかった。 |
必要に応じて ev_break(loop, EVBREAK_ALL); を呼び出すか、タスクが一定回数処理されたら ev_unloop を使用。 |
コンパイル時に -lpthread を忘れた |
リンカエラー | gcc -o example example.c -lev -lpthread でリンク。 |
注意点とベストプラクティス
-
通知は最小限に
ev_async_sendは軽量ですが、過剰に呼び出すとイベントループが頻繁にブレークされ、CPU 使用率が上がります。バッチ処理(複数タスクをまとめてキューに入れ、1 回だけ通知)を意識しましょう。 -
スレッド安全なメモリ
キューに格納するデータは必ず スレッド間で共有可能 なものにし、所有権の移譲を明確にします。特にポインタの解放は、どちらのスレッドが行うか決めておくことが重要です。 -
エラーハンドリング
ev_async_sendは失敗しませんが、キュー操作でmallocが失敗した場合やロック取得に失敗した場合は適切に対処してください。 -
シグナルハンドラとの併用
シグナルハンドラからev_async_sendを呼び出す場合、再入可能な関数(例:write)だけを使用してください。printfなどはシグナルハンドラ内で使用できません。 -
テスト
マルチスレッド環境はレースコンディションが潜むため、Stress テスト(大量タスク・高頻度通知)を行い、デッドロックやメモリリークが無いか確認します。
まとめ
本記事では、libev の ev_async が「他スレッドやシグナルハンドラからイベントループへ安全に通知する」ための機構であることを解説しました。主なポイントは次の通りです。
ev_asyncはev_async_send()で手動通知し、登録コールバックが即座に実行される- マルチスレッド環境でタスクキューと組み合わせると、シンプルかつ高速な非同期処理が実現できる
- キュー操作は Mutex で保護し、
ev_async_sendの呼び出しはロック外で行うことがベストプラクティス
この記事を通じて、読者は 安全かつ効率的に非同期通知を組み込む方法 を習得できたはずです。次回は、ev_prepare や ev_check と組み合わせた 高度なイベントフロー制御 について解説する予定です。
参考資料
- libev 公式リファレンス – ev_async
- POSIX Threads Programming (Pthreads) (O'Reilly)
- 《事件駆動プログラミング入門》 – 高橋亮太 (技術評論社)
