最近、並行性についてよく耳にします。その重要性や、「ソフトウェア開発方法における次の大革命」としての位置づけなどが話題になっています。
では、実際に「並行性」とは何を意味し、F#はどのようにそれを支援できるのでしょうか?
並行性の最もシンプルな定義は、「複数のことが同時に起こり、場合によってはそれらが相互に影響し合う」ということです。一見些細な定義に思えますが、重要なポイントは、ほとんどのコンピュータプログラム(そしてプログラミング言語)が一度に1つのことを処理する直列処理を前提に設計されており、並行性をうまく扱えるようにはなっていないことです。
そして、たとえプログラムが並行性を扱えるように書かれていたとしても、さらに深刻な問題があります。私たち人間の脳は並行性を考える上で苦手意識があるのです。並行性を扱うコードを書くのは非常に難しいとよく言われています。正確に言えば、「正しい」並行コードを書くのが極めて難しいのです!バグのある並行コードを書くのは簡単です。競合状態が発生したり、操作が原子的でなかったりすることがあります。また、タスクが不必要に飢餓状態に置かれたり、ブロックされたりする可能性もあります。これらの問題はコードを見たりデバッガを使ったりしても発見しづらいのです。
F#の具体的な話に入る前に、開発者として扱う必要のある一般的な並行性シナリオをいくつか分類してみましょう。
- 「並行マルチタスキング」:これは、直接制御下にある複数の並行タスク(プロセスやスレッドなど)があり、それらが互いに通信し、安全にデータを共有する必要がある場合です。
- 「非同期」プログラミング:これは、直接制御下にない別のシステムとの対話を開始し、その応答を待つ場合です。ファイルシステム、データベース、ネットワークとの通信がよくある例です。これらの状況は通常I/Oバウンドなので、待っている間に他の有用な処理をしたいものです。このタイプのタスクはしばしば「非決定的」でもあり、同じプログラムを2回実行しても異なる結果が得られる可能性があります。
- 「並列」プログラミング:これは、1つのタスクを独立したサブタスクに分割し、そのサブタスクを並列で実行したい場合です。理想的には、利用可能なすべてのコアやCPUを使います。これらの状況は通常CPUバウンドです。非同期タスクとは異なり、並列処理は通常「決定的」であり、同じプログラムを2回実行しても同じ結果が得られます。
- 「リアクティブ」プログラミング:これは、自分でタスクを開始するのではなく、イベントを監視し、それらをできるだけ早く処理することに焦点を当てる場合です。この状況は、サーバーの設計やユーザーインターフェースの操作時によく発生します。
もちろん、これらは曖昧な定義であり、実際には重複する部分もあります。しかし一般的に、こうしたケースすべてに対処する実際の実装は、主に2つの異なるアプローチを使う傾向があります。
- 待機せずに状態やリソースを共有する必要のある多数の異なるタスクがある場合は、「バッファ付き非同期」設計を使います。
- 状態を共有する必要のない多数の同一タスクがある場合は、「フォーク/ジョイン」や「分割統治」アプローチを使った並列タスクを使います。
並行プログラミングのためのF#ツール
F#は並行コードを書くためのいくつかの異なるアプローチを提供しています。
マルチタスキングと非同期の問題に対しては、F#は
Thread
、AutoResetEvent
、BackgroundWorker
、IAsyncResult
など、.NETでおなじみのものを直接使えます。 しかし、F#はあらゆるタイプの非同期I/Oやバックグラウンドタスク管理のために、「非同期ワークフロー」と呼ばれるより簡単なモデルも提供しています。 これについては次の投稿で詳しく見ていきます。非同期問題に対する別のアプローチとして、メッセージキューと「アクターモデル」を使う方法があります(これは前述の「バッファ付き非同期」設計です)。F#にはアクターモデルの組み込み実装である
MailboxProcessor
があります。 私は、アクターとメッセージキューを使った設計を強く推奨します。これにより様々なコンポーネントが分離され、それぞれを直列的に考えることができるからです。真のCPU並列性に関しては、F#には前述の非同期ワークフローをベースにした便利なライブラリコードがありますし、.NETのTask Parallel Libraryも使えます。
最後に、イベント処理とリアクティブプログラミングに対する関数型アプローチは、従来のアプローチとはかなり異なります。 関数型アプローチでは、イベントを「ストリーム」として扱い、LINQがコレクションを処理するのと同じように、フィルタリング、分割、結合することができます。F#はこのモデルと、標準的なイベント駆動モデルの両方をサポートしています。