TL;DR
- 並行プログラムは同時に起きる複数のものごとをモデル化し、並列プログラムは操作を同時に実行できる部分に分けて実行時間を縮める。
- ミュータブルなデータを複数のオブザーバが見る問題をロックや防衛的コピーで解こうとすると、競合やデッドロックが生まれる。
- Clojure はほぼすべてが値なので、プログラムを関数型モデルと参照型モデルの 2 層に分け、大部分を関数型で書ける。
内容
並行と並列
並行プログラムは、同時に複数のものごとが起きている状況をモデル化する。並列プログラムは、逐次的に実行できる操作を同時に実行可能な部分に分割して、全体の実行時間を縮める。
並行・並列プログラムが必要な理由はたくさんある。
- 過去数十年にわたり、処理性能は主に CPU コアのパフォーマンスの向上でもたらされてきた。しかし近い将来、処理性能はより多くの CPU コアを使うことで得られるようになる。ハードウェアは今まで以上に並行性に富んでおり、その性能を最大限に活かすために、システム自体も並行処理を行う必要がある
- コストの高い計算を実用的な時間内で終わらせるためには、複数コアやマシンで並列実行する必要がある
- リソース待ちで止まっているタスクは、プロセッサを他のタスクに明け渡すべきである
- UI は、裏でコストの高い計算が走っているときでも、入力に応答する必要がある
- 論理的に独立している操作は、プラットフォームがその独立性を認識して利用できるようになっていれば、より簡単に実装できる
並行性の問題点
並行性では、複数のオブザーバ(e.g. スレッド)が 1 つのデータを見ている可能性がある。値と同一実体が分離されていない言語では、これは大きな問題になりうる。このような言語では、操作のたびに以前の記録を削除し、別スレッドに影響を及ぼす可能性がある。
ただし、この問題(複数のオブザーバ)は並行性だけに起因するものではない。たとえば、プログラム中で 2 つの変数が 1 つのデータを参照しているケースもこれに該当する。もしデータがミュータブルならば、変数の状態を追うのが難しくなる。
データがミュータブルになりうる言語では、この問題をロックと防衛的コピー(defensive copying)で解決しようとする。ロックとは、あるオブザーバが値を参照しているときに、別のオブザーバが同じ値を同時に参照できないようにする仕組みである。防衛的コピーとは、あるデータが変更されるのを防ぐためにコピーを作る設計パターンである。
しかし、これらの解決法はスレッドの競合やデッドロックといった別の問題を生み出す。
Clojure における問題の解決法
Clojure ではほぼすべてのものが値なので、コードの大部分を関数型で書ける。そして、ミュータブルを容認するとコードが書きやすくなる限られた範囲についてだけ、参照の 4 つのモデルのどれかを選べばよい。
つまり、プログラムのモデルを 2 つの層に分割する。
- 関数型モデル(functional model): ミュータブルな状態を持たない層。ほとんどのコードはこちらに分類されるはずで、読むのも、テストするのも、並列化するのも簡単である
- 参照型モデル(reference model): 状態をミュータブルとして扱ったほうが(そのデメリットを踏まえても)便利な、アプリケーションの一部分