Future と Promise を使って World の基本原理を実装している。
基本機能として、
- 定期的に与えられた計算を実施する。
- 規定回数の計算を終えたら終了のメッセージを表示する。
- 計算中に例外が発生した場合は、スタックトレースを表示する。
- DoomsDay 例外を発生した場合は、World を停止して終了し、異常とは扱わない。
ややこしい実装になっているのは、busy waiting を避けるため。
next 関数の主な働きは1ステップ分の計算を実施することである。以下が引数の意味。
n: World の状態変数。ここでは 0 からインクリメントする自然数。p: World を生成したmain関数が用意したPromise。main関数はこの Promise からの返事をAwait.ready(p.future, ...)で待っている。- 返り値の型 (
Unit) を明示しているのは、この関数が再帰的に定義されているため。
next 関数は最初に Future により1ステップ分の計算を実施する並行スレッドを生成する。
この Future は、以下の処理を実施する。
nを表示し、nが既定値に達した場合には正常終了し、// p.success(n)TICK_MSマイクロ秒だけ眠り、blocking { Thread.sleep(TICK_MS) }- 次の状態を
n + 1に設定する。
この処理全体を try { ... } catch { case e: Exception => { p.failure(e); n } } で覆うことで実行時エラー (e) を捉え、それを p.failure(e) によって main 関数内の処理に通知するためである。dooms_day(message) も例外で実装されており、通常の例外と DoomsDay は main 関数で区別して扱っている。
Thread.sleep(TICK_MS) を blocking { ... } で覆っているのは、ForkJoinPool の制限を逃れるためである。FJP はブロックするスレッド数がプロセッサ数を越えると破綻するが、blocking 節を用いれば、この制限が及ばないということだ。本例題ではブロックするスレッド数は高々2なのだが、念のため blocking 節を用いている。
main 関数は並行計算のために promise と next(n, p) を実行する future を用意し、その結果を p.future.onComplete 節で設定されたハンドラーで受け取る。
最後の Await.ready(p.future, Duration.Inf) は正常値が返るか、あるいは例外が発生するまで永遠に待ち続けることを意味している。