残りを裏切らずに並行処理を設計する
並行処理は、言語がそれを持つべきだからでなく、本物を作る中で当たった壁が強いた —— 一つのプロセスを共有できない二つの永久 loop。難しいのは、保証を手放さずにそれを加えることで、賭けは、メモリとエフェクトのためにすでに建てた機構 —— 所有権、move、Trivial —— こそが安全な並行処理に要るもので、それをスレッド境界へ拡張したものだ、というものだ。
公で動く言語は、私的なものが後回しにできた問いに直面する:次に何を育てるのか? Mere にとって その答えは願望リストから選ばれたのでなく —— 強いられた。本物のプログラムを建てる中で壁が現れ、 それを越えることは、強い保証を持つ言語が引き受けられる最も難しい feature の一つ —— 並行処理 —— を加えることを意味した。この Part は、前の Part が建てたすべてを裏切らずにそれを加えることについてだ。
それを要求した壁
pain point は具体的で地味だった。HTTP サーバは永久 loop だ —— リクエストを待ち、決して返らない。 第二の永久 loop も要るプログラム —— メッセージチャネルを聴く subscriber、バックグラウンドワーカー、 スケジュールされたジョブ —— は、一度に走らねばならない二つの loop を持ち、そして Mere は一つの プロセスで二つの loop を走らせる術を持たなかった。当座の回避策は、第二の loop を JavaScript ホストへ 押し出し、その event loop に二つを interleave させることだった。それは動くが、告白だ:言語が、本物の プログラムが日常的に要するものを表現できず、だから需要が周囲のランタイムへ漏れ出した。
それは最も純粋な形の dogfood signal だ —— 抽象的に想像された feature ではなく、書けなかった具体的な 何か、それを書こうとして発見された。並行処理は、プログラムが壁に当たったから議題に載った。それは 「言語はスレッドを持つものだ」とはまったく違う正当化だ。その形は繰り返す —— サーバ+ワーカー、 サーバ+cron、サーバ+テレメトリ —— だから、永久に迂回するのでなく、きちんと解く価値があった。
難しい部分:保証を保つ
並行処理は、言語の安全性の約束がこっそり失効する古典的な場所だ。二つのスレッドが可変状態を共有した 瞬間、データ競合を相続する —— タイミングに依存し、ソースに不可視で、大半のテスト実行に不在のバグ。 五つの Part をメモリとエフェクトを明示的で検査可能にすることに費やした言語が、その後スレッドを加え、 それらが何を共有するかについて肩をすくめることはできない。この Part の難しさのすべては、並行処理が 他のすべてと同じくらい明示的で検査されねばならないことにある。
Mere がする賭けは、そこに至るのに新しい安全性の体制を要さない、というものだ。すでに持つ機構こそが
正しい機構だからだ。所有権と move はすでに「この値はいまや他の誰かに属し、あなたは触れない」を
モデル化する。借用注釈はすでに共有読みと排他書きを区別する。Trivial 制約はすでに、片付けなしに
扱えるものを分類する。安全な並行処理は、賭けによれば、その同じアイデアをスレッド境界を越えて運んだ
ものだ:別のスレッドに手渡された値は move された値;二つのスレッドが共有する値は、共有して安全だと
証さねばならない、型システムが一つのスレッド内の共有についてすでに推論するのとちょうど同じように。
並行処理は所有権の物語の拡張になり、その傍らのボルト留めにはならない。
一つでなく五つの問い
「並行処理を加える」は決めるには漠然としすぎる。だからそれは、連載の繰り返す方法で、それぞれ答えられる 五つの精密な問いに分解された:
- 実行モデル —— 本物の OS スレッド、軽量なグリーンスレッド、それとも async タスクか?
- 境界を越える —— ケイパビリティが子スレッドへ行くとき、それは move されるのか、共有されるのか、 コピーされるのか?
- チャネル —— チャネルに流すために、型について何が真でなければならないか?
- Send と Sync —— 「スレッド間を move して安全」と「共有して安全」の trait 的マーカーが要るか、 要るならどれほど重いか?
- region と
withとの相互作用 —— 子スレッドは自分の region を得るか、片付け順序はどうなるか?
そして設計は、エフェクトシステムがそうだったように、それが受理すべきプログラムと拒むべきものを 書くことで釘付けにされた:動く並列計算;親がもう使えないよう子に move された logger(そして再使用は コンパイルエラー);明示的に共有され両方から使える logger;越えて手渡すと型エラーになる単一所有者の ネットワーク接続;チャネルに許される送れる型だけ。受理基準は散文でなくプログラムだ。
絞り込み
答えは、紙上で絞り込まれ、それから本物の実装の前に小さな trial で確かめられ、すべて、既存の設計と整合 する最小のものへ傾いた。
実行モデルは本物の OS スレッド —— 具体的には、ホスト言語 OCaml がすでに提供する並列性の primitive。 それは最小の実装で、実際の CPU コアを直接使い、自前のスケジューラを要さない。async タスクモデルは、 他所ではエレガントだが、ケイパビリティ渡しの決定に逆行するとして棄却された —— それはまさに、エフェクト システムが取り除くために存在する暗黙の制御フローを再導入する。OS スレッドを選ぶことは、なぜ OCaml か の賭けがもう一度報われることだ:ホストのスレッドが Mere のスレッドになる。
境界を越えることは既定で move —— 子に手渡されたケイパビリティは子に属し、親がその後それを使うのは
コンパイル時エラー —— で、自分自身の内部ロックを運ぶ値にだけ明示的な共有。チャネルはその要素型が
送って安全であることを要求する。そして「送って安全」と「共有して安全」の性質は、完全な trait システムを
建てることでなく、二つの構造的述語 —— Send と Sync —— を、言語がすでに追っていた Trivial と
片付けを要する区別の傍らに加えることで扱われる。それはプロジェクトの他と同じ抑制だ:問いに答える最小の
機構、一般的な機械を発明するのでなく、存在するものを再利用する。最後に、spawn された子は自分の新しい
region を得て、親と子のメモリが alias せず、region モデルの保証がスレッド境界を無傷で生き延びる。
ここに完成したものはまだない —— これは形だ、絞り込まれ trial で確かめられた、実装ではなく。だが形こそ
重要なコミットメントだ:Mere の並行処理は拡張された所有権であって、放棄された安全性ではない。次の
回々がそれを建てる。まず、設計全体が乗る二つの述語が実際に動かねばならない —— そしてそれを正しく
することには、微妙な穴があると判った。次回:Send と Sync の型。