止まれない worker
worker pool —— main が jobs を積み、workers が pull して処理し、main が回収する —— が同じ 1 分の内に 2 つの壁に当たった。worker の受信ループは jobs が尽きると永遠にブロックし、止めて join する手段が無い。そしてループが決して返らないので型は bottom になり、C backend はそもそも emit できない。2 つの壁は同じ欠落 primitive だった: channel には「終わった」と言う手段が要り、受信にはそれを聞く手段が要る。
メモリモデルの話群は「意図して残したもの」の一覧で終わり、その筆頭が構造化並行だった。言語は スレッドを spawn しメッセージを渡せたが、並行プログラムがどう終わるかという問いを強制する ものが無かった。この Part はついにそれを強制したアークだ —— 一日 3 本の小さなプログラム、 各々が「終わり」問題の一片を狙う。全体を貫くテーマ: 並行プログラムの難所は動かすことではない。 止めることだ。
1 分差の 2 つの壁
forcing program は並行プログラミングで最もありふれた形にした: worker pool。main が N 個の jobs を channel に積み、W 個の workers が各々 job を pull して結果を push するループを回し、 main が N 個の結果を回収する。書くとほぼ即座に 2 つの壁に当たった。第一は意味論の壁: jobs が 尽きると、各 worker は二度と値を生まないブロッキング受信の中に駐まる —— worker に「もう仕事は 無い」と伝える手段が無いので、止める手段が無く、join する手段が無く、プログラムは綺麗に終われ ない。第二の壁は無関係に見えて、コンパイラの顔をした同じ壁だ: 決して返らない worker ループは bottom 型を持つ —— 型検査器の「この式は何も生まない」の言い方 —— そして C backend はその型の 値の emit を端的に拒む。プログラムは実行時にハングした以前に、コンパイルしなかった。一つの 欠落 primitive が、一度は運用の問題として、一度は型として見えた。
channel を閉じる
その primitive はキューの意味での閉包だ: channel_close は channel を「終わった」と印し、
channel_recv_opt は値をブロック待ちするが、channel が closed かつ drained になると
None を返す。worker のループは match になる: Some job —— 処理してループ、None —— return。
ループはいま終端し、それが全てを一度に直す: worker は unit を返し(bottom でないので compile
する)、pool は handle ごとに join でき、プログラムは中の全スレッドが終わったから終わる。closed
な channel への送信は raise —— それはプログラミングエラーであって、よろけながら続ける状況では
ない —— そして closed & drained への旧来のブロッキング受信も、誰も signal しない条件変数の上で
永遠に眠る代わりに raise する。スコープは意図的だ: interpreter と C backend —— サーバ形の
dogfood が実際に走る場所 —— に実装し、ブラウザ側の 2 backend はその旨を言う compile エラーで
拒否する。pool の example は 4 workers で 12 jobs を回して合計し、そして —— 以前は不可能だった
部分 —— 4 worker 全部を join して exit する。次の話はこの話が救えない worker に会う: まだ
走っていて、ただ終わることを拒む奴だ。