イベントはスレッドに比べて何故ダメなのか (Why Events Are A Bad Idea)
並行処理をプログラミングする方法は大別すると、 スレッドなどを利用した同期処理的な書き方と イベントを利用した非同期処理的な書き方があります (イベントループ)。 最近 C10K問題 が有名になったことや、 Node.js が流行っていて Node.js のメリットとしてイベントモデルであるため C10Kが解決されるというのが上げられていたりして、 イベントモデルのスレッドに対する優位性が注目されることが多いように思います。
しかし個人的な経験として、ある程度以上複雑なプログラムを書く場合、 イベントモデルで非同期処理を多用してプログラムを書くとスレッドで同期処理を使ってプログラムを書いた場合に比べてプログラムの可読性・保守性が著しく悪くなり、 バグが発生しやすくなるように感じます。 しかも並行処理で起こるバグは特定の処理が特定の順序で実行された場合のみ起こるようなことが多く、 再現性が低いのでデバッグが非常に面倒なことになります。 個人的には並行処理の同時処理数が 10K にもならず、 排他制御のための処理のコストやコンテキストスイッチのコストが無視できないほどパフォーマンスが重要ならない 多くのプログラムでは並行処理はイベントモデルではなくスレッドモデルで記述すべきだと思います。
そこでイベントモデルの欠点・デメリットが体系的に整理されている Why Events Are A Bad Idea という2003年に書かれた論文を読んだので要点をまとめてみました。
論文の概要
スレッドモデルの場合、プログラムの状態 (プログラムの実行位置や処理中のデータの内容) はプログラムカウンタとスタックによって管理されます。 プログラムカウンタとスタックを使った状態を管理する部分のロジックは言語処理系が作成してくれます。 また並行している処理はカーネルによってスケジューリングされ、ある程度賢く順番に実行されます。
一方で、イベントモデルではアプリケーションレベルでプログラムの状態管理とスケジューリングは全て自前で行うことになります。 そのためイベントモデルでプログラムを書くとスレッドライブラリ・カーネル・言語処理系の実装によって引き起こされる数々の問題 (例えばスレッド数の上限や、多数のスレッドによるパフォーマンス低下、コンテキストスイッチのコスト) を回避することができます。 その代償として、イベントモデルではスレッドライブラリや言語処理系が提供する非常に便利な機能 (関数呼び出し、例外処理、スケジューリング、デバッガ、etc..) が利用できなくなり、 全て自前で実装し制御する必要があるのでプログラムが非常に複雑になってしまいます。まとめると
-
イベントモデルの長所
- スレッドライブラリ・カーネル・プログラミング言語処理系に存在する数々の問題を自前の実装で回避可能
-
イベントモデルの短所
- スレッドライブラリ・プログラミング言語が提供してくれている便利な機能 (関数呼び出しとか例外処理なども含まれる) が使えない。 全て自前で実装する必要がありプログラムが不必要に複雑になりプログラムの可読性・保守性が落ちる。 自前で実装するのでバグが多くなる。
ということになります。 この論文では2.2で「イベントモデルの長所」として知られている点が実は(あまり大きな)長所ではないとということを、 3章で「イベントモデルの短所」を具体的に述べ、 それによっていかにプログラムが複雑で保守しづらいものになってしまうかを指摘しています。 以下に各章の要点を簡潔にまとめます。
2.1 Duality Revisited
イベントモデルとスレッドモデルは双対である。
2.2 “Problems” with Threads
Performance
- スレッドのスケジューリングのアルゴリズムにまずいところがあると困る。
- 例えば O(n) のアルゴリズムとか(この論文が書かれた2003年時点ではGNU Pthには存在したらしい、2014年現在はどうなのだろう)。
- これはスレッドモデルそのものも問題ではない。
Control Flow
- スレッドモデルだと call/return パターン(普通の関数呼び出し + if-else, loop によるコントロールフロー) しか書けない。自由度が低いという批判が存在する。
-
しかし、そもそもそんな複雑なモデルは使わない (We believe more complex patterns are not used because they are difficult to use well.)。 Control Flow は大別すると
- call/return
- parallel calls
- pipeline
に分けられるが、どれもスレッドモデルで書いたほうがずっと自然に書ける。
Synchronization
- イベントモデルだと synchchronization が不要で単純で済むという主張について。
- それはイベントモデルだとタスクが no preemption (cooperative multitasking) だからだよね。 スレッドも preemption が起こらないような実装 (cooperative thread systems) にしたら同じメリットがあるよ。 しかも multiprocessors の場合は成り立たないよね (とすでに2003年に言われている. 2011年10月に出た iPhone4s からは携帯ですらマルチコアである)
State Management
- スレッドごとに stack 作るから仮想メモリ空間が足りなくなる 。stackサイズを小さくするとstack overflowする。
- イベント方式だとプログラマが state の管理を自前でする必要があるので、無駄なstateが保持されないで済む。
- 論文では言及されてないけど、仮想メモリ不足に関しては64bitになるとどうでもよくなる。
- 2番目の点に関しては、まあイベント特有のメリットと言えるかも。 スレッドでもプログラマが注意すればstateは小さくできるけど、プログラマが注意しないと簡単に無駄なstateが保持されるから。
Scheduling
スケジューリングがアプリケーションレベルで行えるので、 イベント方式だとアプリケーションレイヤの知識を利用してスケジューリングを最適化する余地がある。 例えば同種のイベントをまとめて処理すると code locality を上げられるなど (逆に同じデータは同じプロセッサで処理したほうが data locality が上がってよいケースもあるだろう)。
3 The Case for Threads
大体の場合 concurrent requests はそもそも独立して処理される。
Control Flow
イベントモデルだとプログラム理解する際に プログラマがプログラムを call-return のパターンに頭の中でマッチさせなくてはいけない (The programmer must mentally match these call/return pairs)。 またイベントモデルで書く場合、stateの保持を自分で書かなくてはならならず(stack ripping)、 これがイベントモデルを使う際の大きな重荷となる (実際そう思う)。 あと、control flow が形式的に縛られなくなることで本来1つしか届かないメッセージが複数呼び出される(あるいはコールバックが必要以上に呼び出される) ような問題が起こりやすい (言語やライブラリの仕様としてそういう事態を防ぐのは困難)。 あと既存のデバッグツールとの相性がよい(例えばスタックの状態の表示とか。イベントモデルで自前で状態管理してるとそういったツールからは扱いづらい)。
Exception Handling
例外発生時の処理とかややこしいから言語の機能をそのまま使える thread モデルのほうがよいよねという。 control flow の派生の話.