libuvとは(特にイベントループについて)
http://docs.libuv.org/en/v1.x/design.html を読み進めていった際のメモです。
概要
libuvは「クロスプラットフォームサポートのためのライブラリ」で、Node.jsで使われることを想定して開発が進められてきた(現在はNode.js以外でも用いられている)。
「イベント・ドリブンな非同期I/Oモデル」をもとに設計されていて、以下のようなものを提供している
- プラットフォーム間で異なるI/Oポーリングの仕組みの抽象化
- (ハンドルとストリームによる)ソケットなどの抽象化
- ファイルI/Oやスレッド周辺の各種機能
ハンドルとリクエスト
libuvでは、イベントループとともに、ハンドルとリクエストという2つの抽象化された概念が用意されている。
ハンドルは「(アクティブなときに)何らかの操作を行うことができる、長期的に存在するオブジェクト」を表す。その例としては以下のようなものがある。
- イベントループが一周するたびに一度呼ばれるprepareハンドル
- 新しいコネクションができるたびにコールバックを実行するTCPサーバーハンドル
リクエストは(一般的に)「短い期間で終わる有効な操作」を表す。 リクエストはハンドルを通して行う(例:writeリクエストはハンドルを通してデータを書き込む)ことも、スタンドアロンで行う(例:getaddrinfoリクエストはハンドルを用いず、ループの中で直接実行される)こともできる。
I/Oループ (イベントループ)
I/Oループ(イベントループ)は、libuvの中核をなしている。 イベントループはすべてのI/O操作を管理し、「一つのイベントループが一つのスレッドに対応する」ことが想定されている。
異なるスレッドで動作する限り、複数のイベントループを利用することができる。 ちなみに、イベントループ(そして、ループやハンドルに関係する他のAPI)は、基本的にはスレッドセーフではない。
イベントループは、I/Oに対してシングルスレッドの同期的な(一般的な)アプローチを用いている。 すべての(ネットワーク)I/Oはノンブロッキングなソケット上で行われ、それをプラットフォームに応じた最善の方法でポールしている(Linux上ではepoll, OSXやその他のBSDではkqueue, SunOSではevents port, WindowsではIOCP)。ループの中において、イベントループは(それ以前にポーラーに追加された)ソケット上でのI/Oアクティビティを待ち、ソケットの状況(読み込み可能, 書き込み可能, ハングアップ)とともにコールバックが呼ばれる、これを用いてハンドルはI/O操作を行える。
イベントループへの理解を深めるために、一回のループでどのようなことが行われているのかを確認する。
- 「現在時刻」が更新される。イベントループは、ループの最初に取得した時刻をキャッシュすることで、時刻関係のシステムコールの発行回数を減らしている。
- もしもループが生きているのなら、イベントループの周回がはじまる。もしも生きていないのなら、イベントループは終了する。(有効かつ参照されているハンドル、有効なリクエスト、またはclosingハンドルが存在する場合、イベントループは生きているとみなされる。)
- 適切なタイマーが実行される。イベントループの「現在時刻」よりも前に設定されていたコールバックが呼ばれる。
- pendingコールバックが呼ばれる。多くのI/Oコールバックは、I/Oのポーリングの直後に呼ばれることが多い。しかし、コールバックの実行を(イベントループの)次の周回に遅らせるということもある。そのようにして前の周回で後回しにされたI/Oコールバックが、ここで実行される。
- idleコールバックが呼ばれる。
- prepareハンドルのコールバックが呼ばれる。これはイベントループがI/Oブロックをする直前に呼ばれる。
- pollタイムアウト(どれほどの時間I/Oのブロックを行うか)を計算する。計算は以下の通り行われる。
- 一つ前のステップで計算した時間分だけ、I/Oのブロックをする。(読み書きのために)ファイルディスクリプタを監視しているI/O関連のハンドルはここで適当なコールバックを呼ぶ。
- checkハンドルコールバックが呼ばれる。checkハンドルのコールバックはI/Oのブロックの後に呼ばれ、prepareハンドルのコールバックと対を成している。
- closeコールバックが呼ばれる。もしもハンドルが
uv_close()
によってcloseされた場合、そのハンドルはcloseコールバックを呼び出す。 UV_RUN_NOWAIT
またはUV_RUN_ONCE
モードでループが走っている場合には、ループは終了する (uv_run()
がreturnする)。もしもUV_RUN_DEFAULT
モードでループが走っている場合には、ループが生きてれば次の周回に入り、そうでなければループは終了する。
注1: 7の計算は以下のように行われる
UV_RUN_NOWAIT
フラッグとともにループが走っていた場合、0uv_stop()
が呼ばれている場合(イベントループがもうすぐ止まる場合)には0- アクティブなハンドルやリクエストが存在しない場合には0
- もしもアクティブなidleハンドルが存在する場合には0
- 「closeされるべきハンドル」が存在する場合には0
- 上の場合全てにマッチしない場合には最も直近のタイマーの時間(もしもそれが存在しない場合には無限)
注2: libuvはファイルI/Oにおいてスレッドプールを用いているが、ネットワークI/Oは常にシングルスレッド(イベントループのスレッド)上で行う。
注3: pollingの機構はUnixとWindowsで大きく違うが、libuvは実行モデルに一貫性をもたせている。
ファイルI/O
ネットワークI/Oとは違い、(設計上の困難さから)ファイルI/Oはスレッドプールを用いてファイルのI/Oを行っている。
libuvは現在グローバルな(全てのイベントループが用いることのできる)スレッドプールを用意している。3種類の操作がこのプールで行われる。
- ファイルシステムの操作
- DNS関連の操作(getaddrinfo と getnameinfo)
- ユーザーが定義した操作 (
uv_queue_work()
を利用する)
注: スレッドプールのサイズは限られているので、注意を配る必要がある