libuvの基礎¶
libuvは、非同期、イベント駆動のプログラミング方式を要求します。中心となる役割は、イベントループと、コールバックによる入出力やその他の活動の通知を提供することです。libuvは、タイマー、ノンブロッキングのネットワーク対応、非同期ファイルシステムアクセス、子プロセスなどの基本的なユーティリティを提供します。
イベントループ¶
イベント駆動プログラミングでは、アプリケーションは特定のイベントへの関心を表明し、そのイベントが起きたときに応答します。オペレーティングシステムからイベントを集めたり、他のイベント発生源を監視したりする役割はlibuvが担います。利用者は、イベント発生時に呼び出されるコールバックを登録できます。イベントループは通常、ずっと動き続けます。疑似コードで表すと、次のようになります:
while there are still events to process:
e = get the next event
if there is a callback associated with e:
call the callback
イベントの例を挙げると、次のようなものがあります:
ファイルに書き込む準備ができた
ソケットに読み取り可能なデータがある
タイマーの時間が経過した
このイベントループは、uv_run()にまとめられています。これはlibuvを使う際の中心となる関数です。
システムプログラムで最もよく行うのは、大量の数値計算よりも、入力と出力の処理です。従来の入出力関数(read、fprintfなど)の問題は、これらがブロッキングであることです。ハードディスクへの実際の書き込みやネットワークからの読み取りは、プロセッサーの速度に比べて非常に長い時間がかかります。処理が終わるまで関数が戻らないため、その間、プログラムは何もしていません。高い性能が必要なプログラムでは、他の活動や入出力操作まで待たされることが大きな障害になります。
標準的な解決策の1つは、スレッドを使うことです。それぞれのブロッキング入出力操作を、別のスレッド(またはスレッドプール)で開始します。そのスレッドでブロッキング関数が呼び出されると、オペレーティングシステムは、実際にCPUを必要とする別のスレッドを実行するようスケジュールできます。
libuvが採るのは、別の方式である非同期・ノンブロッキング方式です。現代のほとんどのオペレーティングシステムは、イベント通知の仕組みを提供しています。たとえば、ソケットに対する通常のread呼び出しは、送信側が実際に何かを送るまでブロックします。その代わりに、アプリケーションは、ソケットを監視してイベント通知をキューに入れるようオペレーティングシステムに要求できます。アプリケーションは、都合のよいときにイベントを調べ、データを取得できます(その前に数値計算を行って、プロセッサーを最大限に活用することもできます)。これが非同期なのは、アプリケーションがある時点で関心を表明し、時間的・空間的に別の時点でデータを使うからです。また、ノンブロッキングなのは、アプリケーションのプロセスが自由に他の作業を行えたからです。オペレーティングシステムのイベントをlibuvのイベントの1つとして扱えるため、この方式はlibuvのイベントループ方式によく適合します。ノンブロッキングであることにより、他のイベントも到着する速さに合わせて処理し続けられます[1]。
注記
入出力をバックグラウンドでどのように実行するかは、ここでは問題にしません。ただし、スレッドをプロセッサーの基本単位とするハードウェアの動作方式により、libuvやオペレーティングシステムは通常、バックグラウンドのスレッド・作業スレッドやポーリングを使って、ノンブロッキングで作業を行います。
libuvの主要開発者の1人であるBert Belderが、libuvのアーキテクチャーとその背景を説明する短い動画を公開しています。libuvを使ったことがない方には、短時間で役立つ内容を見ることができます。
libuvのイベントループについては、文書で、より詳しく説明しています。
Hello World¶
基本を確認したところで、最初のlibuvプログラムを書いてみましょう。イベントループを開始して直ちに終了するだけで、他には何もしません。
helloworld/main.c
1#include <stdio.h>
2#include <stdlib.h>
3#include <uv.h>
4
5int main() {
6 uv_loop_t *loop = malloc(sizeof(uv_loop_t));
7 uv_loop_init(loop);
8
9 printf("Now quitting.\n");
10 uv_run(loop, UV_RUN_DEFAULT);
11
12 uv_loop_close(loop);
13 free(loop);
14 return 0;
15}
処理するイベントがないため、このプログラムは直ちに終了します。libuvのイベントループには、各種API関数を使って、イベントを監視するよう指示する必要があります。
libuv v1.0以降では、利用者は、uv_loop_init(uv_loop_t *)で初期化する前に、イベントループ用のメモリを割り当てるべきです。これにより、独自のメモリ管理を組み込めます。uv_loop_close(uv_loop_t *)でイベントループを終了処理してから、記憶領域を削除することを忘れないでください。これらの例では、ループの終了後にプログラムが終了し、システムがメモリを回収するため、イベントループを閉じていません。本番用途のプロジェクト、特に長時間動作するシステムプログラムでは、適切に解放するよう注意すべきです。
既定のイベントループ¶
libuvは既定のイベントループを提供しており、uv_default_loop()でアクセスできます。イベントループが1つだけ必要なら、このループを使うべきです。
default-loop/main.c
1#include <stdio.h>
2#include <uv.h>
3
4int main() {
5 uv_loop_t *loop = uv_default_loop();
6
7 printf("Default loop.\n");
8 uv_run(loop, UV_RUN_DEFAULT);
9
10 uv_loop_close(loop);
11 return 0;
12}
注記
node.jsは、メインのイベントループとして既定のループを使っています。バインディングを書く際は、この点を認識しておいてください。
エラー処理¶
失敗する可能性のある初期化関数や同期関数は、エラーの場合に負の値を返します。失敗する可能性のある非同期関数は、コールバックに状態を表す引数を渡します。エラーメッセージは、UV_E* 定数として定義されています。
uv_strerror(int)とuv_err_name(int)を使うと、それぞれエラーの説明やエラー名を表すconst char *を取得できます。
ファイルやソケットなどの入出力の読み取りコールバックには、引数nreadが渡されます。nreadが0未満なら、エラーが発生しています(UV_EOFはファイル終端のエラーであり、他とは異なる扱いをしたい場合もあるでしょう)。
ハンドルとリクエスト¶
libuvは、利用者が特定のイベントへの関心を表明することで動作します。通常は、入出力デバイス、タイマー、プロセスに対するハンドルを作成して関心を表明します。ハンドルは、uv_TYPE_tという名前の不透明な構造体であり、TYPEはハンドルの用途を表します。
libuvの監視処理
/* Handle types. */
typedef struct uv_loop_s uv_loop_t;
typedef struct uv_handle_s uv_handle_t;
typedef struct uv_dir_s uv_dir_t;
typedef struct uv_stream_s uv_stream_t;
typedef struct uv_tcp_s uv_tcp_t;
typedef struct uv_udp_s uv_udp_t;
typedef struct uv_pipe_s uv_pipe_t;
typedef struct uv_tty_s uv_tty_t;
typedef struct uv_poll_s uv_poll_t;
typedef struct uv_timer_s uv_timer_t;
typedef struct uv_prepare_s uv_prepare_t;
typedef struct uv_check_s uv_check_t;
typedef struct uv_idle_s uv_idle_t;
typedef struct uv_async_s uv_async_t;
typedef struct uv_process_s uv_process_t;
typedef struct uv_fs_event_s uv_fs_event_t;
typedef struct uv_fs_poll_s uv_fs_poll_t;
typedef struct uv_signal_s uv_signal_t;
/* Request types. */
typedef struct uv_req_s uv_req_t;
typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
typedef struct uv_getnameinfo_s uv_getnameinfo_t;
typedef struct uv_shutdown_s uv_shutdown_t;
typedef struct uv_write_s uv_write_t;
typedef struct uv_connect_s uv_connect_t;
typedef struct uv_udp_send_s uv_udp_send_t;
typedef struct uv_fs_s uv_fs_t;
typedef struct uv_work_s uv_work_t;
typedef struct uv_random_s uv_random_t;
/* None of the above. */
typedef struct uv_env_item_s uv_env_item_t;
typedef struct uv_cpu_info_s uv_cpu_info_t;
typedef struct uv_interface_address_s uv_interface_address_t;
typedef struct uv_dirent_s uv_dirent_t;
typedef struct uv_passwd_s uv_passwd_t;
typedef struct uv_utsname_s uv_utsname_t;
typedef struct uv_statfs_s uv_statfs_t;
ハンドルは、長い寿命を持つオブジェクトを表します。そのようなハンドルに対する非同期操作は、リクエストを使って識別します。リクエストは短い寿命を持ち(通常、1つのコールバックにわたってのみ使われます)、一般にハンドル上の1つの入出力操作を表します。リクエストは、個々の操作の開始とコールバックの間でコンテキストを保持するために使います。たとえば、UDPソケットはuv_udp_tで表し、ソケットへの個々の書き込みには、uv_udp_send_t構造体を使います。この構造体は、書き込みが終わった後にコールバックへ渡されます。
ハンドルは、対応する次の関数で設定します:
uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)
コールバックは、監視処理が対象とするイベントが起きるたびに、libuvが呼び出す関数です。アプリケーション固有の処理は、通常コールバック内に実装します。たとえば、入出力の監視処理のコールバックは、ファイルから読み取ったデータを受け取り、タイマーのコールバックは、時間が経過したときに呼び出されます。
Idleハンドルの利用¶
Idleハンドルを使う例を示します。コールバックは、イベントループの反復ごとに1回呼び出されます。Idleハンドルの利用例については、ユーティリティで説明します。Idleの監視処理を使って、監視処理のライフサイクルと、監視処理が存在するためにuv_run()がブロックする様子を見てみましょう。カウントが所定の値に達するとIdleの監視処理が停止し、アクティブなイベント監視処理がなくなるため、uv_run()が終了します。
idle-basic/main.c
#include <stdio.h>
#include <uv.h>
int64_t counter = 0;
void wait_for_a_while(uv_idle_t* handle) {
counter++;
if (counter >= 10e6)
uv_idle_stop(handle);
}
int main() {
uv_idle_t idler;
uv_idle_init(uv_default_loop(), &idler);
uv_idle_start(&idler, wait_for_a_while);
printf("Idling...\n");
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
uv_loop_close(uv_default_loop());
return 0;
}
コンテキストの保存¶
コールバックを使うプログラミング方式では、呼び出し元とコールバックの間で、アプリケーション固有の情報である「コンテキスト」を渡したくなることがよくあります。すべてのハンドルとリクエストには、void* dataメンバーがあり、ここにコンテキストを設定して、コールバック内で元の型にキャストできます。これは、Cのライブラリ全般でよく使われるパターンです。また、uv_loop_tにも同様のdataメンバーがあります。
もちろん、ハードウェアの処理能力によります。