mp::iothreads

マルチコア時代の高速サーバーの実装で紹介したアーキテクチャを実装しようとすると、アーキテクチャ自体はマルチスレッドなので、ロックだ、メモリ管理だと、いろいろと面倒です。一般化できるなら一般化して、ロジック部分だけを書けば高速なサーバーが実装できるようにしたい。

そこで、面倒なところを実装したライブラリmp::iothreadsを開発しています。高速なイベント駆動ライブラリ mpioをベースとしています。高速さを重視し、C++で実装しています。
スレッド間通信や送信用バッファなどの面倒を見ます。各所でいろいろと"ひとひねり"しています。


まずInputの段階は、

  1. accept(2)する
  2. accept(2)したファイルディスクリプタをイベント待ちリストに加える
  3. epoll/kqueueなどでソケットが読み込み可能になるまで待つ
  4. 実際に読み込む & バッファリングする
  5. ストリームパーサでプロトコルをパースする
  6. Dispatchする

という流れで実装できるのですが、accpet(2)が問題になります。1つのポートからaccept(2)したファイルディスクリプタを複数のスレッドで待ち受けることになるので、

  • どのスレッドに割り当てるか
  • 割り当てることになったスレッドにどうやってファイルディスクリプタを受け渡すか

ということが悩みの種です。

前者は、ファイルディスクリプタの番号は連番で振られることを利用して、ファイルディスクリプタをスレッドの数で割った剰余を使えば、そこそこ均等に負荷分散されることが期待できそうです。

後者の問題は、Input用スレッドはepoll/kqueue/...でブロックしているところにあります。つまり、キュー + pthread_mutex + pthread_cond という一般的なスレッド間通信手法が使えない。この問題は一般的にはどうやって解決しているのでしょうか…。

  • ロック付きキューで送ったあと、自分にシグナルを送ってepoll/kqueue/...を中断させる
  • ロック付きキューで送ったあと、ダミーのpipeをepoll/kqueue/...でイベント待ちさせておき、そのpipeに何かデータを送る
  • pipeをepoll/kqueue/...でイベント待ちさせておき、そのpipeにファイルディスクリプタ番号を流す

という方法がありそうです。mp::iothreadsでは3番目の方法を実装しています。(mp::fdnotify



続いてDispatchですが、主な実装上のポイントは、関数を変数に入れるにはどうするかという問題と、その変数の寿命管理になります。
いろいろとありますが、まぁ何とかしています:-p(mp::callback_message


DispatchとLogicの間のスレッド間通信は、普通に キュー + pthread_mutex + pthread_cond で実装しています。(mp::blocking_vector


最後にOutputですが、カーネル内のバッファに入りきらないほど大きいデータを送りたいときは、スレッドを使って送ればブロックしないので性能が良くなるはずですが、小さなデータを送るときは、スレッド間通信とコンテキストスイッチのオーバーヘッドとの影響が大きく出てしまうかもしれません。(スレッド間通信はInputと同じようにpipeを使うので、たぶん遅い)
というわけで、小さなデータはLogicを走らせているスレッドで直接送信します。データが大きければOutput用のスレッドに受け渡しますが、このときにバッファのコピーが発生しないように、多少工夫しています。データの送信はwritev(2)で行われます。


少々規模が大きいライブラリなので安定するまでまだ時間がかかると思いますが、内部で利用している個々のライブラリ(mp::event、mp::sparse_array、mp::fdnotify、mp::blocking_vector、mp::zone、mp::objet_callbackなど)はだいたい安定して使えます。
マルチコア時代の高速サーバーの実装のサンプルコードなども見てみてください。