ライブラリAPIのシングルスレッド化

高負荷環境で(mosh concurrent)なスレッドを使うと何か間違ったことが起こるようなので、とりあえず単なる入力多重化のために使っていたスレッドを廃止する方向でAPIを修正していくことにした。

canvasでの例

例えばcanvasレベルでのイベントハンドリングは廃止し、内部の抽象化レイヤを独立したAPIとして公開し、イベントソースの異なるeventはユーザ側でどうにかする方向に。
Windowsは、そのWindowクラスを登録したスレッドでしかイベントを処理することができない。そこで、window-masterというスレッドが、全てのWindowの生成と開放、イベントハンドリングを対応し、Windowを開いたスレッドへのイベント配送を行っていた。
今後moshのスレッドを使わずにこれを実現するには、window-masterをCなりC++で書き直す必要が有る。

canvasはあまり多重化が重要なタイプのAPIで無いので(そもそもバックエンドにする描画エンジンがスレッドセーフであることが保証されていない)、今後は完全にシングルスレッドのAPIとしてデザインしなおすことになる。
しかし、通信周りは多重化できないと困るので、何らかの方法でスレッドを使わず多重化する手法を考えないといけない。

継続によるI/O多重化

幸い、Schemeには継続があるので、これをI/O多重化に使用できる。つまり、I/O待ちに入る直前に継続を保存し、I/Oが直ぐ終わることが保証される状況になったら継続を復元して実際のI/Oを行えば良い。
問題は2つある。

  • 継続の再開のトリガ
  • 並列して他の処理を実行できること

これは1つの方法で解決できる。I/O待ちの"ステーション"を作り、それをFFIで制御できるようにする。

ステーション

この"ステーション"は、例えば、libeventではevent_baseと呼ばれる。
ここで言うステーションとは、いくつかのオブジェクトを纏めて待ち受けることで非同期I/Oを同期I/Oに変換する仕組みで、大概のOSではそのような仕組みが用意されている。
原理的に、一つのステーションは一種類のオブジェクトしか待ち受けることが出来ない。

  • epollステーション → Linuxのfd
  • WaitForMultipleObjectステーション → Win32のHANDLE

他にlibusbのようにライブラリ固有のステーションを持つものも有る。
簡単には、Schemeスクリプトはステーションに対してblockし、利用可能になったオブジェクトに関連付けられた継続を再開する。
これは単純にportに置き換えることは出来ない。例えば、listenしているsocketは、読み出し可能になったときにデータではなくsocketを出力する*1

FFIレベルスレッドの導入

Scheme側は同時に1つのステーションしか待ち受けることが出来ないが、ステーションは1種類のオブジェクトしか待ち受けることが出来ない。しかも、WaitForMultipleObjectのように一度に待ち受けることの出来るオブジェクトに制限があることもある。
そこで、ステーションを合成してステーションにする仕組みがFFI側に必要になる。もちろん、これはmoshのスレッドで簡単に書くことが出来るが、FFIレベルでスレッドを持つことにもメリットが有る。FFIスレッドはGCによって停止することが無いし、多分moshのスレッドより安定して動作する。
より重要な目的は、同期I/Oを非同期I/Oに変換し、非同期I/O同様の多重化を提供することに有る。
FFIレベルスレッドは :

  • "FFIステーション"の実装(要するにpthreadやWin32 thread)
  • FFI通知機構の提供(要するに条件変数)
  • 専用のコールバック機構
  • Cクロージャ

を備える必要が有る。
コールバックをそのまま使うことはできない。BoehmGCは登録されていないスレッドからのGC起動が行えない。
Cクロージャは、"C関数をあるパラメタで起動するプログラム"へのポインタで、これはlibffiで簡単に生成できる。

prereading

いくつかのAPIは、読み出し可能というシグナルが必ずしも読み出し可能であることを保証しない。例えば、socketがaccept可能になってから処理されるまでの間にsocketが破棄されるなどの問題が有る。
いわゆるnon-blocking I/Oが使える場合は、確実にこれらのオブジェクトにnon-blocking I/Oを適用することで問題を回避できる。
しかし、non-blocking I/Oが使用できず、かつ、読み出し可能かどうかを検討する確実なAPIが存在しないケースも有る。例えばlibusb-0.XのAPIはこのケースに該当する。
このようなケースでは、FFIスレッド側で読み出しの完了までを面倒見ることで問題を回避できる。 libusb固有の問題として、libusbは読み出しサイズにセンシティブ(余計なサイズのバッファを予め与えることが出来ないAPI)なので読み出しの完了までを行うwrapperはどちらにせよ必要になる。

*1:yuniはこの目的のために一般化されたportを持つ