えちょ記

語らないブログ

副作用から考える並行処理とアクターモデル

相変わらずいきなり仕事が忙しくなっているので、また関数言語の話。
「関数言語は副作用が無いから並行処理に向いている」とよく言われます。ですが、そもそも副作用ってなんなんだ?という本質的なところで理解できないことが多いんじゃないかなーと。そもそも私もその辺分かっているのか疑問なので、ちょっと考察してみる試みを。

副作用とは

プログラミングにおける副作用(ふくさよう)とは、ある機能がコンピュータの(論理的な)状態を変化させ、それ以降で得られる結果に影響を与えることをいう。(Wikipediaより)

これは多分逆に考えたほうが分かりやすい。「副作用が無い」というのは、同じ仕事をさせたら、いつも同じ成果を出すクールなやつということ。私のようにそのときの気分でやることが変わるのでは駄目なのです。
厳密には、以下の二つの条件、

  • 「同じ条件を与えれば必ず同じ結果が得られる」
  • 「他のいかなる機能の結果にも影響を与えない」

参照透過性(Wikipedia)

を満たす処理を「副作用が無い」と言います。この定義だと、デバッグ用のログを出力するような関数である場合、計算結果自体は変わらなくても「ログ出力」という副作用が発生していると言えますね。

副作用が無いと並行処理が出来るんだって

結論から進めますが、関数に副作用が無い場合、相互の計算に順序の依存が無ければ計算の数だけ並行処理が可能です。「(18+43)×(32+85)」という計算の場合、「18+43」と、「32+85」の計算については、一人でやっても二人でやってもかまいません。ただ、掛け算についてはこの2つの計算結果が揃わないと計算できないですね。計算順序の依存があるからです。
余談ですが、上記例のような非常に原始的な基礎演算については、CPUによっては自動的に並行処理出来る範囲を探し出して並行処理を行ったり(アウトオブオーダー実行)、コンパイラが並行処理が出来る範囲を見つけ出して並行処理を行うプログラムに変換したりするものがあります。

閉じたセカイ

さて、話を副作用の定義に戻します。副作用の定義のうち、「他のいかなる機能の結果にも影響を与えない」について考えて見ましょう。これをものすごく超訳的に人間関係に置き換えると、「他人に迷惑をかけない」と言い換えることが出来ます。「18+43」の計算を「無言で」行う事が出来る人は副作用の無い人、「じゅう〜はち!、たす!、よんじゅーさん!は!はちたすさんで、じゅーいち!、いちたすよんにいちくりあがって、ろく!ぜんぶでろくじゅーいち!!!」などと叫んで行う人は副作用のある人です。
が、これは他人から見たときの話。「計算している人」にとっては、どんな手順だろうと計算ミスをしない範囲において、その計算には副作用は発生しません。つまり、副作用と見なされる要素のうち、外界に影響を与える効果については、自分ひとりの閉じた世界に限れば全て無視できます。外界への影響を無視できればその分副作用となる要素が減り、結果として並行処理できる要素が増えることになります。

喋るだけなら副作用は無い

そして通信の話。通信には「送信」と「受信」の2つの要素があります。このうち「送信」は、先ほどの話で言う「外界に影響を与えること」です。世の中にはビックマウスと呼ばれる人種の方がいっぱいいます、すごくうらやましいです。で、ビックマウスの方のうち言うだけで行動が他人任せの人がいます。人格的にはあんまりお勧めできませんが、並行処理においては有利に働きます。彼らは面倒な副作用要素をどんどん吐き出す(送信)することで、自分にとって副作用が無い行動をどんどん進めることが出来るのです。

聞き取り作業は副作用の交差点

対して「受信」は、副作用そのものを凝縮した行動となります。「何が来るかわからない」「いつ来るのか分からない」「何が来るかによってやることが変わる」‥‥まるで人生の交差点?他人が捨てていった副作用のゴミをどんどん拾い上げては燃えるゴミ、燃えないゴミと振り分ける、そんな裏方作業が待っています。
ですが、こうやって副作用のゴミを一手に引き受ける人や場所があるほうが、システム全体としては見通しがよくなります。並行処理において、下手にいろんな人があちこちで副作用を処理しようとすると、誰が何をするのか分からなくなって全体への指示が滞り、指示ミス(バグ)も多くなります。これが従来型のプログラム技法です。それに対して通信による副作用の送受信を行うと、副作用の発生箇所を受信箇所に集約し、人々のやるべきことを分かりやすくし並行処理にまつわるトラブルを劇的に減らすことが可能となります。これが「アクターモデル」と呼ばれる並行処理の概念です。

アクターモデルは時代のトレンド

さて、「人」とは、プログラムにおける「プロセス」、意思の疎通は「プロセス間通信」です。特に自分の言った内容について、相手が聞き取ったかどうか確認せずにどんどん次のことを行うモデルを「非同期処理」といいます。「アクターモデル」においてプロセス間通信は「メッセージパッシング」とも言います。アクターモデルでは人の行動は聞くこと(受信)により始まり、一旦動き出したら次に受信する必要が発生するまで行動を止めません。仮に他人にやってもらう行為が発生しても、言うだけ言ったら(送信)他人の都合お構い無しに自分がやれることをどんどん進めます。
並行処理の効率は、最大何人が参加できるか(並行処理可能性)、意思疎通が簡単かどうか(プログラムの開発効率)、行動に対する意思の疎通の割合をどれだけ減らせるか(通信量と計算量のバランス)で決まります。アクターモデルは意思疎通領域における問題点の整理に大きく貢献し、更に副作用の切り出しが容易な言語と組み合わせることで並行処理可能性の引き上げに役立ちます。
関数型言語は基本的に副作用の切り出しに特化しているため、本質的にアクターモデルの適用が容易ですが、副作用さえ切り出せれば関数型言語にこだわる必要はありません。このアクターモデル&関数型の副作用追い出し原則を言語レベルで全面採用したのが、最近巷で話題となっているerlangです。erlang では更に、システムダウンに繋がる要素を言語設計と言語ランタイム、更に基本ライブラリやミドルウェアレベルで徹底的に追い出すことで、高度な並行処理とシステム稼働率99.9999999%(1年で31msのダウンタイム)を両立した案件開発を行うための強力な武器となっています。

並行処理へのパラダイムシフト

C# など主流言語においても関数型の要素の取り込みと同時にアクターモデルへの移行が活発に研究されています。マイクロソフトのMidoriプロジェクトとか、調べてみるともう、非同期処理・アクターモデル関数型言語・言語レベルの安全性保証を活用した単一メモリ空間など、ソフトウェア開発/設計における時代の最先端のオンパレードで言語オタクには溜まらない領域ですよ!
これらの概念は単に最先端というだけでなく、CPUの性能向上における制限 (1コアの処理能力の限界)に単を発した、もう既に始まっているパラダイムシフトに必要とされるものです。マルチコアで有効に動くアプリケーションを開発するためには、設計段階でこれらの概念を理解しておく必要があります。100人以上の同時アクセス、特に、上限が分からないユーザー数が同時にアクセスする可能性がある案件に関わるつもりであれば、頭の片隅くらいには今のうちに知識に加えておく方がいいかも?
ハード側のパラダイムシフトが進むことは既に決定しました。今、ソフトウェアの進化が問われています。