Elixirのプロセスは分離されていて、デフォルトでは何も共有しません。リンクされていない子プロセスがクラッシュしても、親プロセスは影響を受けません。
この動作は、プロセス同士を_リンク_することで変えられます。2つのプロセスがリンクされていると、一方のプロセスの障害がもう一方のプロセスに伝わります。リンクは双方向です。
spawn_link/1を使うと、呼び出し元のプロセスにリンクされた状態でプロセスを生成できます。これはアトミックな操作です。または、あとからProcess.link/1でリンクすることもできます。
プロセスをリンクしておくと、並列化した作業を行うときに役立ちます。ある作業のかたまりが完了しなかったときに、ほかのかたまりを続行したくない、という場面です。
リンクは、プロセスを_監視_するためにも使えます。あるプロセスが_終了をトラップ_していると、リンクされているプロセスがクラッシュしても、自分はクラッシュしません。代わりに、クラッシュに関するメッセージを受け取ります。これにより、たとえばクラッシュしたプロセスを再起動するなど、クラッシュにうまく対処できます。
プロセスは、Process.flag(:trap_exit, true)を呼び出すことで終了をトラップするように設定できます。Process.flag/2は、フラグの新しい値ではなく_古い_値を返す点に注意してください。
リンクされたプロセスがクラッシュしたときにそのプロセスに送られるメッセージは、{:EXIT, from, reason}というパターンに一致します。ここでfromはPIDです。reasonが:normalというアトム以外であれば、そのプロセスがクラッシュしたか、強制終了されたことを意味します。
タスクは、1つの特定の操作を実行するためのプロセスです。通常はほかのプロセスと通信しませんが、タスクを開始したプロセスに結果を返すことはできます。
タスクは、作業を並列化するのによく使われます。
async/await
タスクを開始するには、Task.async/1を使います。これは無名関数を引数に取り、呼び出し元のプロセスにリンクされた新しいプロセスでそれを実行します。%Task{}構造体を返します。
実行結果を取得するには、%Task{}構造体をTask.await/2に渡します。タスクが終了するのを待って、その結果を返します。第2引数はミリ秒単位のタイムアウトで、デフォルトは5000です。
タスクを開始してから待機するまでの間、タスクを開始したプロセスはブロックされず、ほかの操作を行える点に注意してください。
Task.async/1で開始したタスクは、呼び出し元のプロセスにメッセージを送るため、必ず待機する必要があります。Task.await/2は、各タスクに対して1回しか呼び出せません。
start/start_link
副作用だけを目的としてタスクを開始したい場合は、Task.start/1またはTask.start_link/1を使います。Task.start/1は呼び出し元のプロセスにリンクされていないタスクを開始し、Task.start_link/1は呼び出し元のプロセスにリンクされたタスクを開始します。どちらの関数も{:ok, pid}というタプルを返します。
_Instruments of Texas_での実験的なRPN電卓の開発は続いています。チームはいくつかの試作機を作りました。量産できる最良のものを選ぶために、これらは徹底的な検査を受ける必要があります。
これから2種類のチェックを行います。
1つ目は信頼性チェックです。検査対象の電卓がクラッシュするか、十分な速さで応答しない入力を検出します。障害を切り分けるために、各入力の計算は別々のプロセスで実行する必要があります。呼び出し元のプロセスでリンクと終了のトラップを使うと、計算が完了したのかクラッシュしたのかを検出できます。
2つ目は正しさのチェックです。与えられた入力に対して、電卓が返す結果が期待どおりかどうかを確認します。信頼性チェックにすでに合格した電卓だけが正しさのチェックを受けます。そのため、クラッシュの心配はありません。ただし、処理を速くするために操作は並行して実行する必要があります。これは非同期タスクにうってつけの場面です。
RPNCalculatorInspection.start_reliability_check/2関数を実装します。この関数は2つの引数を取ります。1つは関数(電卓)、もう1つは電卓への入力です。入力と、生成されたプロセスのPIDを含むマップを返します。
生成されたプロセスは、与えられた入力で与えられた電卓関数を呼び出します。このプロセスは、呼び出し元のプロセスにリンクされます。
RPNCalculatorInspection.start_reliability_check(fn _ -> 0 end, "2 3 +")
# => %{input: "2 3 +", pid: #PID<0.169.0>}
RPNCalculatorInspection.await_reliability_check_result/2関数を実装します。この関数は2つの引数を取ります。1つ目の引数は、信頼性チェックの入力と、その入力の信頼性チェックを実行しているプロセスのPIDを含むマップです。これはRPNCalculatorInspection.start_reliability_check/2が返すものです。2つ目の引数は、異なる入力に対する信頼性チェックの結果を蓄積するマップです。
この関数は終了メッセージを待ちます。
信頼性チェックを実行している同じプロセスから、理由が:normalの終了メッセージ({:EXIT, from, reason})を受け取った場合、結果のマップのキーinputに値:okを追加して返します。
信頼性チェックを実行している同じプロセスから、それとは異なる理由の終了メッセージを受け取った場合、結果のマップのキーinputに値:errorを追加して返します。
100ms以内にこれらの条件に一致するメッセージを1つも受け取らなかった場合、結果のマップのキーinputに値:timeoutを追加して返します。
# when an exit message is waiting for the process in its inbox
send(self(), {:EXIT, pid, :normal})
RPNCalculatorInspection.await_reliability_check_result(
%{input: "5 7 -", pid: pid},
%{}
)
# => %{"5 7 -" => :ok}
# when there are no messages in the process inbox
RPNCalculatorInspection.await_reliability_check_result(
%{input: "3 2 *", pid: pid},
%{"5 7 -" => :ok}
)
# => %{"5 7 -" => :ok, "3 2 *" => :timeout}
RPNCalculatorInspection.reliability_check/2関数を実装します。この関数は2つの引数を取ります。1つは関数(電卓)、もう1つは電卓への入力のリストです。
リストの各入力について、start_reliability_check/2を使って新しいリンクされたプロセスで信頼性チェックを開始します。次に、このようにして開始した各プロセスについて、await_reliability_check_result/2を使ってその結果を待ちます。
プロセスを開始する前に、終了メッセージを受け取れるように、現在のプロセスに終了をトラップするフラグを立てる必要があります。その後、このフラグを元の値に戻します。
この関数は、すべての入力の信頼性チェックの結果を含むマップを返します。
fake_broken_calculator = fn input ->
if String.ends_with?(input, "*"), do: raise("oops")
end
inputs = ["2 3 +", "10 3 *", "20 2 /"]
RPNCalculatorInspection.reliability_check(fake_broken_calculator, inputs)
# => %{
# "2 3 +" => :ok,
# "10 3 *" => :error,
# "20 2 /" => :ok
# }
RPNCalculatorInspection.correctness_check/2関数を実装します。この関数は2つの引数を取ります。1つは関数(電卓)、もう1つは電卓への入力のリストです。
リストの各入力について、与えられた入力で電卓を呼び出す非同期タスクを開始します。次に、このようにして開始した各タスクについて、100msのあいだその結果を待ちます。
fast_cheating_calculator = fn input -> 14 end
inputs = ["13 1 +", "50 2 *", "1000 2 /"]
RPNCalculatorInspection.correctness_check(fast_cheating_calculator, inputs)
# => [14, 14, 14]