學習軌道
/
Elixir
Elixir
/
練習
/
RPN 計算機檢查
RPN 計算機檢查

RPN 計算機檢查

學習練習

簡介

Elixir 的行程彼此隔離,預設不會共用任何東西。未連結的子行程崩潰時,不會影響到它的父行程。

只要將行程互相_連結_,就能改變這種行為。如果兩個行程互相連結,其中一個行程發生失敗時,失敗會傳播到另一個行程。連結是雙向的。

使用spawn_link/1可以產生一個已經連結到呼叫端行程的行程,這是原子操作;或者也可以在之後用Process.link/1將它們連結起來。

在進行平行化的工作時,如果某個工作區塊失敗而無法完成,其他區塊就不該繼續做下去,這時候將行程連結起來會很有用。

攔截結束訊號

連結也可以用來_監督_行程。如果行程_攔截結束訊號_,那麼當與它連結的行程崩潰時,它不會跟著崩潰,而是會收到一則關於這次崩潰的訊息。這讓它能優雅地處理崩潰,例如重新啟動崩潰的行程。

呼叫Process.flag(:trap_exit, true),就能把行程設定為攔截結束訊號。請注意,Process.flag/2回傳的是旗標的_舊_值,而不是新的值。

當連結的行程崩潰時,傳送給該行程的訊息會符合{:EXIT, from, reason}這個模式,其中from是 PID。如果reason是:normal以外的原子,就代表該行程崩潰了,或是被強制終止。

Tasks

任務是行程,用途是執行某個特定的操作。 它們通常不會和其他行程溝通,但可以把結果回傳給啟動該任務的行程。

任務通常用來將工作平行化。

async/await

要啟動任務,請使用Task.async/1。它接受一個匿名函式作為引數,並在一個連結到呼叫端行程的新行程中執行它。它會回傳一個%Task{}結構。

要取得執行結果,請把%Task{}結構傳給Task.await/2。它會等待任務完成,然後回傳結果。第二個引數是逾時時間,單位為毫秒,預設值為 5000。

請注意,在啟動任務和等待任務之間,啟動任務的行程不會被封鎖,可以去做其他操作。

用Task.async/1啟動的任務都應該要等待,因為它會傳送訊息給呼叫端行程。每個任務只能呼叫Task.await/2一次。

如果你只想為了副作用而啟動任務,請使用Task.start/1或Task.start_link/1。Task.start/1會啟動一個沒有連結到呼叫端行程的任務,而Task.start_link/1則會啟動一個連結到呼叫端行程的任務。這兩個函式都會回傳{:ok, pid}元組。

說明

你在 Instruments of Texas 的實驗性 RPN 計算機工作仍在繼續。你的團隊打造了幾個原型,這些原型需要經過徹底的檢查,才能選出最適合量產的那一個。

你想進行兩種類型的檢查。

首先,可靠性檢查會偵測受檢計算機在哪些輸入下會崩潰,或是回應得不夠快。為了隔離故障,每個輸入的計算都必須在獨立的行程中執行。你可以透過在呼叫端行程中建立連結並攔截退出,來偵測計算是完成了還是崩潰了。

其次,正確性檢查會檢查對於給定的輸入,計算機回傳的結果是否符合預期。只有已經通過可靠性檢查的計算機才會進行正確性檢查,所以不必擔心崩潰。不過,這些運算應該並行執行以加快整體流程,這讓它成為非同步任務的完美使用案例。

1. 為單一輸入啟動可靠性檢查

實作 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>}

2. 解讀可靠性檢查的結果

實作 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}

3. 為多個輸入執行並行的可靠性檢查

實作 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
#     }

4. 為多個輸入執行並行的正確性檢查

實作 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]
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Elixir Exercism

準備好開始 RPN 計算機檢查 了嗎?

註冊 Exercism,透過 58 個概念168 個練習 和真人引導來學習並精通 Elixir,全部免費。