uvbook -- libuvの仕組みとidle-basicの解説

目次

libuvのチュートリアルuvbook,第2節のIdlingプログラムを解説します.libuvNode.jsの根幹部分を担うCライブラリです.IdlingプログラムのLispによる実装例は後ほど別ページとして投稿します.間違い,コメント等は@waterloo_jpまで.

下のコードは,uvbookのIdleハンドル使用例です.ここでは下のコード例を追いながら,Idleハンドルの使い方と,libuv内の仕組みについて説明します.コードとMakefileはGitHubに置いています.

#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;
}

libuvの基本はイベントループと非同期処理です.イベントループの実態は単なるwhileループです.イベントループに登録されている「ハンドル」や「リクエスト」をwhileループ内で起動していきます.多くの入出力系「ハンドル」が非同期で処理され,処理の終了とともにコールバックが起動されます.

上のコード例では非同期処理は使われていませんが,20行目のuv_run()がイベントループを開始ます.イベントループは登録されている処理がなくなるまでループを続けます.

イベントループ内で処理するタスクは「ハンドル」と「リクエスト」にカプセル化します.この例では,Idleハンドルにタスクをカプセル化しています.Idleハンドルは,イベントループの周回毎にタスクを起動したい時に使います.

14行目で宣言されているuv_idle_t idlerがIdleハンドル・オブジェクトです.後ほど,ハンドルの初期化からコールバックの登録まで詳細を見ていきますが,今のところ17行目でコールバックを登録し,6–11行目で定義されるwait_for_a_while()関数がコールバックとしてイベントループの周回毎に呼び出されることを理解していれば十分です.

libuvの基本的プログラミング手順は以下のとおりです.

  1. ループの初期化(16行目のuv_default_loop()).
  2. イベントループ内で実行するハンドルの準備(16–17行目でIdleハンドルをセットアップ).
  3. イベントループを開始(20行目のuv_run()).
  4. ループ・オブジェクトの後処理(22行目のuv_loop_close()).

ループ・オブジェクトの初期化

まず始めに,libuvの根幹であるイベント・ループを管理するループ・オブジェクトを初期化する必要があります.

uvbook for Lisp – Hello Worldのコード例6–7行目ではuv_loop_init()を呼び出し,自分でループ・オブジェクトを初期化しました.しかし,ここの例ではループ・オブジェクトの初期化コードが見当たりません.それは,16行目のuv_idle_init()の第1引数のuv_default_loop()で行われています.

    uv_idle_init(uv_default_loop(), &idler);

uv_default_loop()は最初の呼び出しでループ・オブジェクトを初期化しそのポインタを返し,2度目以降の呼び出しでは,初期化済みオブジェクトへのポインタを返します.

ハンドルとリクエスト

コード例で使われているIdleハンドルを見ていく前に,libuvにおける「ハンドル」と「リクエスト」の違いについて説明します.

非同期処理の実行はイベントループに依頼します.libuvでは,何度もイベントループから起動される処理を「ハンドル」オブジェクトで管理し,1回で終える処理を「リクエスト」オブジェクトで管理します.

これを書いている時点ではまだ全てのハンドルとリクエストを調べていませんが,ハンドルとリクエストを合わせて非同期処理を管理する場合もあるようです(この辺はuvbookを読み進めてまた更新します).

「ハンドル」にバインドされているコールバック関数は,「ハンドル」が有効化(activate)されている間定期的に起動されます.つまり,「ハンドル」はイベントを発し続けるevent emitterのような役割を果たします.それに対し「リクエスト」は,非同期処理が終了した時に1回だけ呼ばれるコールバックを保持しているので,Promiseオブジェクトの様な役割を果たします1

Idleハンドル

Idleハンドル(uv_idle_tオブジェクト)は,イベントループ1周毎に必ず起動されます.必ず呼ばれるのにIdleという名称はちょっとおかしな気もしますが,IO処理がメインスレッドをブロックしないように,あえてIdleコールバックを挿入して,ループをアイドリング状態にして並列処理を前に進めるために使ったりするからです.後ほどループの説明をしますが,Idleハンドルが有効化(active)されている時は,イベントループはIOのポーリング処理を待たずに処理を継続します.

Idleハンドルの初期化は16行目で行われます.

    uv_idle_init(uv_default_loop(), &idler);

ここで,idlerにイベントループのポインタが登録されます.「libuvの内部実装を覗く」のセクションでuv_idle_init()関数の中身を説明しますが,ここでは特にメモリの割当は行われず,uv_idle_t型構造体のフィールドに必要な情報を設定するだけです.

「ハンドル」はジョブの完了とともに実行すべきコールバックを保持しています.Idleハンドルの場合は,実行すべきジョブがないので,直ちにコールバックが呼び出されます.

Idleハンドルの有効化(activation)とコールバックの登録は17行目で行われます.

    uv_idle_start(&idler, wait_for_a_while);

uv_idle_start()内で,イベントループ2が管理するIdleハンドル用キューにIdleハンドルが追加されます.キューと言っても実態はリンクリストです.

uv_idle_start()はすでに有効化されているハンドルに対しては何もせず処理を返すので,複数回呼んでも問題ありません.

イベントループの開始

uv_run()関数の内部にはwhileループがあります.このwhileループが,libuvのイベントループの正体です.uv_run()の第2引数にはランモード(uv_run_mode型のenum)を指定します.ランモードは以下の3種類があります.

  • UV_RUN_DEFAULT: 有効なハンドルとリクエストが無くなりwhileループが終了するまでuv_run()関数は返りません.whileループの終了はイベントループの終了を意味します.
  • UV_RUN_ONCE: イベントループ1回で終了します.実装を調べてからここの記述を後日更新しますが,入出力のハンドルがprocess.nextTick()を起動しコールバックを次のループにペンディングしない限り,イベントループのスレッドは入出力処理の終了までブロックされるようです.
  • UV_RUN_NOWAIT: これも実装を調べてから更新しますが,UV_RUN_ONCEと違い,コールバックがペンディングされなくてもブロックせずすぐにループから脱するようです.動作中の非同期処理がどうなるのか後日実装を調べてみます.

GUIプログラミングでは,全ての処理をイベントループの前に書き,イベントループが始まったら,プログラムはイベント・ドリブンで動きますが,libuvも同じで,uv_run()の前に全てのプログラム・ロジックを記述しておきます.

イベントループ内の動作

経過時間で起動するTimerハンドルのために,イベントループはまず始めに経過時間をアップデートします.その後,Timerハンドルを処理します.

次に,前のループで残っていた(pendingされていた)非同期処理を実行します.その後,Idleハンドルを処理します.イベントループオブジェクトに管理されているIdleハンドル用Queueから,Idleハンドルが取り出され,コールバック関数が呼び出されます.この呼び出しはブロッキング処理なので,コールバック関数内の処理に時間をかけてはいけません.

ファイル読込みのような1回で終了するリクエストの場合,ハンドルの役目はタスクと共に終了し,イベント・キューから削除されますが,Idleハンドルはuv_idle_stop()関数が呼ばれるまで生き続けます.上のidle-basicプログラムでは10,000,000回コールバックが呼ばれた時点でIdleハンドルが終了します.それまでは,コールバックが終了する度にIdleハンドルは一旦Queueから取り除から,再びQueueに追加され次のループに備えます.

この後いよいよ,ブロッキングが生じるIO系の処理が起動されるのですが,その前後に必要な処理を記述できるように,IO処理の前にPrepareハンドルが,そして後にCheckハンドルがそれぞれ処理されます.これらは形式的にはIdleハンドルと似ていますが,呼び出されるタイミングがIO処理を挟む形で呼び出されます.

IO系の処理は,libuvによって別スレッドで並列処理されます.これについての詳しい解説はまた別ページで行います.

イベントループの終了

イベントループは順次イベント・キューからハンドルを取り出し,ハンドルにバインドされたコールバックを呼び出します.イベントループがUV_RUN_DEFAULTモードで開始された場合,イベント・キューが空になるとイベントループは終了し,アプリケーションが終了します.

ループ・オブジェクトの後片付け

22行目でループ・オブジェクトのクリーンアップが行われます.ここではループ内で起動されたスレッドの停止や,排他制御のロックがクリーンアップされます.

    uv_loop_close(uv_default_loop());

libuvの内部実装を覗く

ここでもう少しlibuvの内部実装を見てみましょう.まずIdleハンドルの初期化と有効化はloop_watcher.cファイルの引数付きマクロUV_LOOP_WATCHER_DEFINE(idle, IDLE)で定義されています.

uv_idle_init()の処理

まず初期化のuv_idle_init()が呼ばれると,uv__handle__init()が内部で呼び出されます.これはuv_common.hで以下のように定義されたマクロです.ただし,(h)はハンドルオブジェクトへの,loop_はループオブジェクトへのポインタで,type_UV_IDLE定数です.

uv_idle_init()ではuv_idle_t型構造体のフィールドに適切な情報を設定し,イベントループのオブジェクトが管理しているハンドラのキューにIdleハンドラを登録します.メモリの割当てなどは行われません.

(h)->loop = (loop_);
(h)->type = (type_);
(h)->flags = UV__HANDLE\_REF;  /* Ref the loop when active. */
QUEUE_INSERT_TAIL(&(loop_)->handle_queue, &(h)->handle_queue);
uv__handle_platform_init(h);

最初の3行でハンドルに,ループオブジェクトのポインタ,ハンドルタイプ,ハンドルが初期化されたフラグ,をそれぞれ設定しています.UV__HANDLE_REFは0x20の定数でビット演算子でハンドルの状態をテストできるようにしています.

QUEUE_INSERT_TAIL()では,ループオブジェクトが管理しているハンドル用キュー((loop_)->handle_queue)の最後尾にこのハンドル(&(h)->handle_queue)を追加しています.キューはqueue.h内のマクロで実装されています.キューという名が付いていますが,実際にはリンク・リストで,キュー内の任意の位置のハンドルがアクセスされたり,取り出されたりします.

uv_idle_start()の処理

次にハンドルの有効化手続きを見てみましょう.uv_idle_start()の定義は以下の通りです.

int uv_idle_start(uv_idle_t* handle, uv_idle_cb cb) {
  if (uv__is_active(handle)) return 0;
  if (cb == NULL) return -EINVAL;
  QUEUE_INSERT_HEAD(&handle->loop->idle_handles, &handle->queue);
  handle->idle_cb = cb;
  uv__handle_start(handle);
  return 0;
}

ハンドルが有効化されると,先ほど設定したUV__HANDLE_REFに加えてUV__HANDLE_ACTIVEのビット(0x40)が立ちます.そこで最初に起動済みハンドルかどうかチェックしています(2行目).コールバックが指定されていない場合はエラー値を返します(3行目).

次にQUEUE_INSERT_HEAD()loop->idle_handlesを先頭とするキューの頭にこのハンドルを挿入します(4行目).idle_handlesキューは以下のように変化します.

したがって今回有効化するIdleハンドルは,初期化時に追加されたloop->handle_queueと,今回挿入されたloop->idle_handlesの,2つのキューに存在することになります.

最後のuv__handle_start(handle)で,ループが管理しているアクティブ・ハンドル数loop->active_handlesが1インクリメントされます.アクティブ・ハンドル数はイベントループの終了条件に使われます.

uv_run()の処理

uv_run()関数内部のwhileループがイベントループの実体です.uv_run()core.cファイル内で定義されています.whileループの実装は以下のようになっています.

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);
    ran_pending = uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    uv__io_poll(loop, timeout);
    uv__run_check(loop);
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE implies forward progress: at least one callback must have
       * been invoked when it returns. uv__io_poll() can return without doing
       * I/O (meaning: no callbacks) when its timeout expires - which means we
       * have pending timers that satisfy the forward progress constraint.
       *
       * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
       * the check.
       */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

(執筆中….)

uv_loop_close()の処理

uv_loop_close()uv_common.cで定義され,機種依存のクリティカルな処理はloop.c内のuv__loop_close()関数に委譲しています.

ここではハンドルのキューを走査し,未終了のハンドルがないかチェックし,起動したスレッドを終了し,排他制御のロックを後片付けします.

(続く…..)

注釈


  1. Event emitterとPromiseの比喩はLXJS2012でのBert Belderの講演に拠ります. ↩︎

  2. uv_default_loop() が返すポインタの参照先で uv_loop_t 型の構造体. ↩︎