Elixir 的行程彼此隔離,預設不會共用任何東西。未連結的子行程崩潰時,不會影響到它的父行程。
只要將行程互相_連結_,就能改變這種行為。如果兩個行程互相連結,其中一個行程發生失敗時,失敗會傳播到另一個行程。連結是雙向的。
使用spawn_link/1可以產生一個已經連結到呼叫端行程的行程,這是原子操作;或者也可以在之後用Process.link/1將它們連結起來。
在進行平行化的工作時,如果某個工作區塊失敗而無法完成,其他區塊就不該繼續做下去,這時候將行程連結起來會很有用。
連結也可以用來_監督_行程。如果行程_攔截結束訊號_,那麼當與它連結的行程崩潰時,它不會跟著崩潰,而是會收到一則關於這次崩潰的訊息。這讓它能優雅地處理崩潰,例如重新啟動崩潰的行程。
呼叫Process.flag(:trap_exit, true),就能把行程設定為攔截結束訊號。請注意,Process.flag/2回傳的是旗標的_舊_值,而不是新的值。
當連結的行程崩潰時,傳送給該行程的訊息會符合{:EXIT, from, reason}這個模式,其中from是 PID。如果reason是:normal以外的原子,就代表該行程崩潰了,或是被強制終止。
任務是行程,用途是執行某個特定的操作。 它們通常不會和其他行程溝通,但可以把結果回傳給啟動該任務的行程。
任務通常用來將工作平行化。
async/await
要啟動任務,請使用Task.async/1。它接受一個匿名函式作為引數,並在一個連結到呼叫端行程的新行程中執行它。它會回傳一個%Task{}結構。
要取得執行結果,請把%Task{}結構傳給Task.await/2。它會等待任務完成,然後回傳結果。第二個引數是逾時時間,單位為毫秒,預設值為 5000。
請注意,在啟動任務和等待任務之間,啟動任務的行程不會被封鎖,可以去做其他操作。
用Task.async/1啟動的任務都應該要等待,因為它會傳送訊息給呼叫端行程。每個任務只能呼叫Task.await/2一次。
start/start_link
如果你只想為了副作用而啟動任務,請使用Task.start/1或Task.start_link/1。Task.start/1會啟動一個沒有連結到呼叫端行程的任務,而Task.start_link/1則會啟動一個連結到呼叫端行程的任務。這兩個函式都會回傳{:ok, pid}元組。
你在 Instruments of Texas 的實驗性 RPN 計算機工作仍在繼續。你的團隊打造了幾個原型,這些原型需要經過徹底的檢查,才能選出最適合量產的那一個。
你想進行兩種類型的檢查。
首先,可靠性檢查會偵測受檢計算機在哪些輸入下會崩潰,或是回應得不夠快。為了隔離故障,每個輸入的計算都必須在獨立的行程中執行。你可以透過在呼叫端行程中建立連結並攔截退出,來偵測計算是完成了還是崩潰了。
其次,正確性檢查會檢查對於給定的輸入,計算機回傳的結果是否符合預期。只有已經通過可靠性檢查的計算機才會進行正確性檢查,所以不必擔心崩潰。不過,這些運算應該並行執行以加快整體流程,這讓它成為非同步任務的完美使用案例。
實作 RPNCalculatorInspection.start_reliability_check/2 函式。它應該接受 2 個引數:一個函式(也就是計算機),以及一個給計算機的輸入。它應該回傳一個 map,其中包含該輸入以及衍生行程的 PID。
衍生出的行程應該以給定的輸入呼叫給定的計算機函式。該行程應該與呼叫端行程建立連結。
RPNCalculatorInspection.start_reliability_check(fn _ -> 0 end, "2 3 +")
# => %{input: "2 3 +", pid: #PID<0.169.0>}
實作 RPNCalculatorInspection.await_reliability_check_result/2 函式。它應該接受兩個引數。第一個引數是一個 map,內含可靠性檢查的輸入,以及針對這個輸入執行可靠性檢查之行程的 PID,也就是 RPNCalculatorInspection.start_reliability_check/2 回傳的內容。第二個引數是一個 map,用來累積不同輸入的可靠性檢查結果。
這個函式應該等待退出訊息。
如果它從執行可靠性檢查的同一個行程收到原因為 :normal 的退出訊息({:EXIT, from, reason}),就應該回傳結果 map,並在input鍵之下加入值:ok。
如果它從執行可靠性檢查的同一個行程收到原因不同的退出訊息,就應該回傳結果 map,並在input鍵之下加入值:error。
如果在 100 毫秒內沒有收到符合這些條件的訊息,就應該回傳結果 map,並在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 個引數:一個函式(也就是計算機),以及一個給計算機的輸入陣列。
對於陣列中的每一個輸入,它應該使用start_reliability_check/2在一個新的連結行程中啟動可靠性檢查。接著,對於每一個這樣啟動的行程,它應該使用await_reliability_check_result/2等待其結果。
在啟動任何行程之前,這個函式需要將目前的行程標記為攔截退出,才能接收退出訊息。之後,它應該把這個標記重設回原本的值。
這個函式應該回傳一個 map,內含所有輸入的可靠性檢查結果。
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 個引數:一個函式(也就是計算機),以及一個給計算機的輸入陣列。
對於陣列中的每一個輸入,它應該啟動一個非同步任務,以給定的輸入呼叫計算機。接著,對於每一個這樣啟動的任務,它應該等待其結果 100 毫秒。
fast_cheating_calculator = fn input -> 14 end
inputs = ["13 1 +", "50 2 *", "1000 2 /"]
RPNCalculatorInspection.correctness_check(fast_cheating_calculator, inputs)
# => [14, 14, 14]