設計概要¶
libuvは、もともと、Node.jsのために書かれた、クロスプラットフォームの支援ライブラリです。イベント駆動の非同期I/Oモデルを中心に設計されています。
このライブラリは、異なるI/Oポーリング機構を単純に抽象化する以上の機能を提供します。「ハンドル」と「ストリーム」は、ソケットなどを扱う高水準の抽象化を提供します。また、クロスプラットフォームのファイルI/Oやスレッド機能なども提供します。
次の図は、libuvを構成する各部分と、それらが関係するサブシステムを示しています。
ハンドルとリクエスト¶
libuvは、イベントループと組み合わせて使う、ハンドルとリクエストという2つの抽象化を提供します。
ハンドルは、アクティブな間に特定の操作を行える、長く存続するオブジェクトを表します。例を挙げます。
prepareハンドルは、アクティブな間、ループの反復ごとに1回コールバックが呼び出されます。
TCPサーバーのハンドルは、新しい接続が発生するたびに接続コールバックが呼び出されます。
リクエストは、通常は短時間で終わる操作を表します。ハンドルに対する操作もあり、writeリクエストはハンドルへデータを書き込むために使います。独立した操作もあり、getaddrinfoリクエストはハンドルを必要とせず、ループ上で直接実行されます。
ハンドルとリクエストを扱う際の指針は次のとおりです。
ハンドルの初期化で、uv_foo_init()が成功した場合は、必ず、
uv_close()を呼び出してください。ハンドルの初期化関数がエラーになった場合は、何もする必要はありません。閉じるのはハンドルだけで、リクエストは閉じません。たとえば、
uv_tcp_tはハンドルで、uv_write_tはリクエストです。ハンドルのメモリーを解放または再利用できるのは、
uv_close_cbの中、またはその後だけです。それより前にはできません。多くのハンドルには、初期化と開始・停止の関数がありますが、ないものもあります。たとえば、
uv_tcp_tと、uv_process_tでは異なります。uv_spawn()は、ハンドルの初期化とプロセスの開始を1つにまとめています。リクエストは、完了時、または、
uv_cancel()でキャンセルされたときに、自動的に閉じられます。追加の後処理が必要なのは、
uv_fs_tと、uv_getaddrinfo_tのリクエストだけです。uv_fs_tの場合は、使い終わったら、uv_fs_req_cleanup()を呼び出してください。uv_getaddrinfo_tの場合は、uv_freeaddrinfo()です。リクエストのメモリーを解放または再利用できるのは、その時点以降だけです。
I/Oループ¶
I/Oループ、つまりイベントループは、libuvの中心となる部分です。すべてのI/O操作の場を提供し、1つのスレッドに結び付けて使うことを意図しています。各ループを別々のスレッドで実行すれば、複数のイベントループを実行できます。libuvのイベントループや、ループ・ハンドルを扱う他のAPIも、特記がない限り、スレッドセーフではありません。
イベントループは、一般的な、単一スレッドによる非同期I/Oの方式に従います。すべてのネットワークI/Oは非ブロッキングソケット上で行い、プラットフォームで利用できる最適な機構でポーリングします。Linuxではepoll、OSXや他のBSDではkqueue、SunOSではevent ports、WindowsではIOCPです。ループの反復の中で、ポーラーに追加したソケットのI/O活動を待つためにブロックし、ソケットの状態(読み取り可能、書き込み可能、切断)を示すコールバックを呼び出します。これにより、ハンドルは読み取り、書き込み、その他の必要なI/O操作を行えます。
イベントループの動作を理解しやすくするため、次の図で、ループの1回の反復の全段階を示します。
ループが認識する「現在時刻」を最初に設定します。
ループを、
UV_RUN_DEFAULTで実行した場合は、期限に達したタイマーを実行します。ループが認識する、現在時刻より前に実行を予定した、アクティブなすべてのタイマーのコールバックを呼び出します。ループが、存続中なら反復を開始し、そうでなければ、すぐに終了します。では、ループはいつ、存続中とみなされるのでしょうか。アクティブで参照されているハンドル、アクティブなリクエスト、または閉じる処理中のハンドルがある場合は、存続中とみなされます。
保留中のコールバックを呼び出します。通常、I/OコールバックはI/Oのポーリング直後に呼び出されます。ただし、次のループの反復へ延期される場合もあります。前の反復で延期したI/Oコールバックがあれば、この時点で実行します。
idleハンドルのコールバックを呼び出します。名前は紛らわしいですが、アクティブなidleハンドルは、ループの反復ごとに実行されます。
prepareハンドルのコールバックを呼び出します。prepareハンドルは、ループがI/Oでブロックする直前にコールバックが呼び出されます。
ポーリングのタイムアウトを計算します。I/Oでブロックする前に、どれだけの時間ブロックするかを計算します。タイムアウトの計算規則は次のとおりです。
ループを、
UV_RUN_NOWAITフラグで実行した場合、タイムアウトは0です。ループが停止しようとしている場合、つまり、
uv_stop()が呼び出された場合、タイムアウトは0です。アクティブなハンドルもリクエストもない場合、タイムアウトは0です。
アクティブなidleハンドルがある場合、タイムアウトは0です。
閉じる処理を待つハンドルがある場合、タイムアウトは0です。
以上のどれにも該当しない場合は、最も近いタイマーまでの時間を使います。アクティブなタイマーがなければ、無限に待ちます。
ループがI/Oでブロックします。この時点では、前の段階で計算した時間だけI/Oでブロックします。特定のファイルディスクリプターの読み取りや書き込みを監視していた、I/O関連のすべてのハンドルのコールバックを、この時点で呼び出します。
checkハンドルのコールバックを呼び出します。checkハンドルは、ループがI/Oでブロックした直後にコールバックが呼び出されます。checkハンドルは、基本的にprepareハンドルに対応するものです。
closeコールバックを呼び出します。
uv_close()を呼び出してハンドルを閉じた場合、closeコールバックが呼び出されます。ループが認識する「現在時刻」を更新します。
期限に達したタイマーを実行します。「現在時刻」は、次のループの反復まで再び更新されないことに注意してください。そのため、他のタイマーを処理している間に期限に達したタイマーは、次のイベントループの反復まで実行されません。
反復を終了します。ループを、
UV_RUN_NOWAITまたは、UV_RUN_ONCEのモードで実行した場合、反復が終了し、uv_run()が戻ります。UV_RUN_DEFAULTで実行した場合は、まだ、存続中なら先頭から続行し、そうでなければ終了します。
重要
libuvは、非同期のファイルI/O操作を実現するためにスレッドプールを使いますが、ネットワークI/Oは、常に各ループのスレッドという1つのスレッドで実行します。
注記
ポーリングの機構は異なりますが、libuvはUnix系システムとWindowsの間で、一貫した実行モデルを提供します。
ファイルI/O¶
ネットワークI/Oと違い、libuvが利用できる、プラットフォーム固有のファイルI/Oプリミティブはありません。そのため、現在はスレッドプールでブロッキングのファイルI/O操作を実行しています。
クロスプラットフォームのファイルI/Oの状況について詳しく知るには、この記事を参照してください。
libuvは現在、すべてのループが作業をキューに入れられる、グローバルなスレッドプールを使っています。このプールでは、現在、次の3種類の操作を実行します。
ファイルシステム操作
DNS関数(getaddrinfoとgetnameinfo)
次を通じたユーザー指定のコード:
uv_queue_work()
警告
詳しくは、スレッドプールでの作業のスケジューリングの節を参照してください。ただし、スレッドプールのサイズはかなり限られていることに注意してください。