三個新概念:link、exit signal、system process。
process link
當 process A 依賴 process B,就需要對 B 的健康狀態保持注意。要達到此目的有兩種作法,使用 link BIF 或是使用 monitor。
當 A 呼叫 link(B) 時,兩個 process 就會互相監控,如果 A死了,B就會收到 exit signal,反之亦然。此機制可作用在單一 node ,也可以用在分散式的 erlang 系統中。
當一個行程收到 exit signal 時,如果沒有做特別的處理,接收者也會離開,但是行程可以攔截這些 exit signal,當一個行程有捕捉離開訊號,就被稱為「系統行程」。
如果連結到系統行程的行程離開,發出了 exit signal,系統行程不會跟著自動終結,反而會接收離開訊號,並作對應的處理。
on_exit 處理器
如果一個行程離開,想要作某些動作,可以寫一個 on_exit(Pid, Fun) 處理器
process_flag(trap_exit, true) 會將生成的行程轉成系統行程,參考 edoc:process_flag
當 trap_exit 設定為 true,process 收到 exit signal 後會轉成訊息 {'EXIT', From, Reason},然後就能被當成一般的訊息處理。
當 trap_exit 設定為 false,process 收到 normail 以外的 exit signal 時,process 就會死亡並將 exit signal 傳送給 linked processes。
on_exit(Pid, Fun) ->
spawn(fun() ->
% 會將這個新生成的行程轉成系統行程
process_flag(trap_exit, true),
% 將新的 process 連結到另一個 process: Pid
link(Pid),
% 行程 Pid 死亡時,此新的行程會收到 exit signal,Pid 是被連結的行程,剛剛才死亡
receive
{'EXIT', Pid, Why} ->
Fun(Why)
end
end).
測試,因為 F 無法處理 atom,刻意傳送 atom 造成 Pid 死亡。
1> F = fun() ->
receive
X -> list_to_atom(X)
end
end.
#Fun<erl_eval.20.80484245>
2> Pid = spawn(F).
<0.34.0>
3> lib_misc:on_exit(Pid, fun(Why) -> io:format(" ~p died with:~p~n",[Pid, Why]) end).
<0.36.0>
4> Pid ! hello.
<0.34.0> died with:{badarg,[{erlang,list_to_atom,[hello],[]}]} hello
5>
=ERROR REPORT==== 24-Jan-2014::11:21:50 ===
Error in process <0.34.0> with exit value: {badarg,[{erlang,list_to_atom,[hello]
,[]}]}
遠端處理 process 錯誤
因為某個行程的錯誤,會發送出來,並交給另一個行程處理,而 erlang 的行程可以運作在多個機器節點上,也就是說,不受機器的限制,而能遠端處理另一台機器節點上的 process 錯誤,這是 erlang 的容錯精神。
錯誤處理的細節
erlang 錯誤處理的三個底層的概念
連結 link
link 定義了兩個行程間錯誤遞送的路徑,兩個連結在一起的行程,其中一個死掉時,另一個會收到離開訊息。目前連結到某個行程的所有行程的集合,稱為 link set離開訊號 exit signal
exit signal 是行程死亡前產生的,此訊號會廣播到 link set 的所有行程,此信號包含了一個參數,說明死亡的原因,該原因可能是 erlang 任何一種 term。可利用 exit(Reason) 設定原因,或是讓錯誤發生時自動設定。例如除以0的錯誤原因會是 badarith
當一個行程成功地估算它的函數,正常把工作做完,就會以 normal 的原因死亡。
行程 Pid1 可以藉由 exit(Pid2, X) 主動送出一個 exit signal 給 Pid2,但 Pid1 並沒有真的死亡。Pid2將會收到 {'EXIT, Pid1, X'} 的訊息,就像是 Pid1 死亡產生了離開的訊息一樣。因此 Pid1 可以假裝自己已經死亡了。
系統行程 system process
當行程收到非正常的離開訊號,它也會死亡,除非它是系統行程。當 system process 收到來自 Pid 的離開訊號 Why,該訊號會被轉換成 {'EXIT', Pid, Why},並加入到系統行程的 mailbox 中。
呼叫 process_flag(trap_exit, true) 會將一個正常的行程轉換成 system process
當離開訊號到達行程時,會根據訊號內容與是否為系統行程,而有不同的處理方式:
trap_exit | exit signal | 動作 |
---|---|---|
true | kill | 死亡: 對 link set 廣播此 killed 離開訊號 |
true | X | 產生 {'EXIT', Pid, X} 到 mailbox |
false | normal | 繼續: 不處理 |
false | kill | 死亡: 對 link set 廣播此 killed 離開訊號 |
false | X | 死亡: 對 link set 廣播此 X 離開訊號 |
kill 這個離開的原因,會產生一個無法被捕捉的離開訊號,收到 kill 的行程一定會死亡,不管是不是 system process。
OTP 裡面的 supervisor process 會用這個功能來殺死一些流氓 rogue 行程。
捕捉離開訊號的慣例
- 不在意自己建立的行程有沒有死掉
單純地用 spawn 產生行程Pid = spawn(fun() -> ... end)
如果自己建立的行程死掉了,我也不想活了
更明確的說法是,如果自己建立的行程,不正常離開時,我也不想活了
建立行程的process不能被設定為要捕捉離開訊號,且要用 spawn_link 產生行程Pid = spawn_link(fun() -> ... end)
如果自己建立的行程死掉了,我想要處理錯誤
使用 spawn_link 與 trap_exitsprocess_flag(trap_exit, true), Pid = spawn_link(fun() -> ... end), ... loop(...), loop(State) -> receive {'EXIT', SomePid, Reason} -> loop(State1); end.
捕捉離開訊號的進階資訊
這會開始三個process A,B,C。A連結到 B,B連結到C,A會捕捉 exit signal,且觀察 B 的離開。如果 Bool 為 true,B會捕捉離開訊號。當離開的原因為 M ,C 會死亡。
-module(edemo1).
-export([start/2]).
start(Bool, M) ->
A = spawn(fun() -> a() end),
B = spawn(fun() -> b(A, Bool) end),
C = spawn(fun() -> c(B, M) end),
% 這是為了要讓 C 死亡時,列印資料出來才加的
% 正式的程式不應該使用 sleep 來達成同步
sleep(1000),
status(b, B),
status(c, C).
a() ->
process_flag(trap_exit, true),
wait(a).
b(A, Bool) ->
process_flag(trap_exit, Bool),
link(A),
wait(b).
c(B, M) ->
link(B),
case M of
{die, Reason} ->
exit(Reason);
{divide, N} ->
1/N,
wait(c);
normal ->
true
end.
wait(Prog) ->
receive
Any ->
io:format("Process ~p received ~p~n",[Prog, Any]),
wait(Prog)
end.
sleep(T) ->
receive
after T -> true
end.
status(Name, Pid) ->
case erlang:is_process_alive(Pid) of
true ->
io:format("process ~p (~p) is alive~n", [Name, Pid]);
false ->
io:format("process ~p (~p) is dead~n", [Name,Pid])
end.
測試1:B 是正常行程,C 執行了 exit(abc),B 會死亡,死亡後再廣播,A會收到訊息 {'EXIT',<0.34.0>,abc}。
1> edemo1:start(false, {die, abc}).
Process a received {'EXIT',<0.34.0>,abc}
process b (<0.34.0>) is dead
process c (<0.35.0>) is dead
ok
測試2:B 是正常行程,C 執行了 exit(normal),B 收到 C 正常離開的訊號,沒有死亡
2> edemo1:start(false, {die, normal}).
process b (<0.38.0>) is alive
process c (<0.39.0>) is dead
ok
測試3:B 是正常行程,C 產生了算術錯誤,B 會死亡,然後廣播,A 會收到訊號,並轉換為訊息 {badarith, ...
3> edemo1:start(false, {divide,0}).
Process a received {'EXIT',<0.42.0>,
{badarith,
[{edemo1,c,2,
[{file,
"d:/projectcase/erlang/erlangotp/src/edemo1.erl"},{line,39}]}]}}
=ERROR REPORT==== 24-Jan-2014::15:20:06 ===
Error in process <0.43.0> with exit value: {badarith,[{edemo1,c,2,[{file,"d:/projectcase/erlang/erlangotp/src/edemo1.erl"},{line,39}]}]}
process b (<0.42.0>) is dead
process c (<0.43.0>) is dead
ok
測試4:B 是正常行程,C 執行了 exit(kill),B 會死亡,死亡後再廣播,A會收到訊息 {'EXIT',<0.34.0>,killed}。
4> edemo1:start(false, {die,kill}).
Process a received {'EXIT',<0.46.0>,killed}
process b (<0.46.0>) is dead
process c (<0.47.0>) is dead
ok
測試5:將 B 改成系統行程,重複上面四個測試,B 都會捕捉來自 C 的錯誤
5> edemo1:start(true, {die, abc}).
Process b received {'EXIT',<0.51.0>,abc}
process b (<0.50.0>) is alive
process c (<0.51.0>) is dead
ok
6> edemo1:start(true, {die, normal}).
Process b received {'EXIT',<0.55.0>,normal}
process b (<0.54.0>) is alive
process c (<0.55.0>) is dead
ok
7> edemo1:start(true, {divide,0}).
Process b received {'EXIT',<0.59.0>,
{badarith,
[{edemo1,c,2,
[{file,
"d:/projectcase/erlang/erlangotp/src/edemo1.erl"}, {line,39}]}]}}
=ERROR REPORT==== 24-Jan-2014::15:25:23 ===
Error in process <0.59.0> with exit value: {badarith,[{edemo1,c,2,[{file,"d:/pro
jectcase/erlang/erlangotp/src/edemo1.erl"},{line,39}]}]}
process b (<0.58.0>) is alive
process c (<0.59.0>) is dead
ok
8> edemo1:start(true, {die,kill}).
Process b received {'EXIT',<0.63.0>,kill}
edemo1:start(true, {die,kill}).process b (<0.62.0>) is alive
process c (<0.63.0>) is dead
ok
如果把 c/2 改成下面這樣,以 exit(B,M) 將 B 停止,觀察 A、B、C 的狀況
c(B, M) ->
process_flag(trap_exit, true),
link(B),
exit(B, M),
wait(c).
測試
1> edemo2:start(false, abc).
Process c received {'EXIT',<0.34.0>,abc}
Process a received {'EXIT',<0.34.0>,abc}
process a (<0.33.0>) is alive
process b (<0.34.0>) is dead
process c (<0.35.0>) is alive
ok
2> edemo2:start(false, normal).
process a (<0.37.0>) is alive
process b (<0.38.0>) is alive
process c (<0.39.0>) is alive
ok
3> edemo2:start(false, kill).
Process c received {'EXIT',<0.42.0>,killed}
Process a received {'EXIT',<0.42.0>,killed}
process a (<0.41.0>) is alive
process b (<0.42.0>) is dead
process c (<0.43.0>) is alive
ok
4> edemo2:start(true,abc).
Process b received {'EXIT',<0.47.0>,abc}
process a (<0.45.0>) is alive
process b (<0.46.0>) is alive
process c (<0.47.0>) is alive
ok
5> edemo2:start(true,normal).
Process b received {'EXIT',<0.51.0>,normal}
process a (<0.49.0>) is alive
process b (<0.50.0>) is alive
process c (<0.51.0>) is alive
ok
6> edemo2:start(true,kill).
Process c received {'EXIT',<0.54.0>,killed}
Process a received {'EXIT',<0.54.0>,killed}
process a (<0.53.0>) is alive
process b (<0.54.0>) is dead
process c (<0.55.0>) is alive
ok
跟錯誤處理相關的函數
@spec spawn_link(Fun) -> Pid
建立行程時,同時建立兩個 process 的連結,這兩個是不能分割的步驟,所以並不等同於 spawn 之後再 link@spec process_flag(trap_exit, true)
可將行程設定為系統行程@spec link(Pid) -> true
建立對稱雙向的連結,在 A 上 link(B),跟在 B 上 link(A) 結果是一樣的,如果 Pid 不存在,會產生 noproc 例外,連結關係只會產生一次,不會重複兩次@spec unlink(Pid) -> true
將 目前行程 與 Pid 行程 之間的所有連結移除@spec exit(Why) -> none()
會造成目前的行程以 Why 原因終結,如果執行此敘述的時候,不在 catch 裡面,那麼此行程就會以 Why 為參數,廣播離開訊號到 link set@spec exit(Pid, Why) -> true
送出離開訊號到 Pid ,原因是 Why@spec erlang:monitor(process, Item) -> MonitorRef
設定一個監控器,Item 是行程的 Pid 或是 註冊名稱
erlang 的容錯機制
容錯必須至少要有兩台機器,一個失敗,另一個馬上接手。
erlang 的容錯機制就是,一個行程做事,另一個監控,出錯時馬上接手。在分散式erlang中,「做事的行程」和「監控的行程」可以放在不同的機器上。
此模式稱為 worker-supervisor
monitor
因為 link 是雙向的,如果要阻止其中一個死亡,就要設定為 system process,但有時不想這樣處理,就可以使用 monitor。
monitor 是非對稱連結,如果 process A 監控 process B,B 死亡會送 exit signal 給 A ,但 A 死亡,不會送訊號給 B。
keep-alive process
要讓一個 process 一直活著,如果死亡就馬上重新啟動。
keep_alive 會估算 Fun,啟動一個行程,並註冊名稱為 Name,同時會以 on_exit,產生新的 process 監控 Name 行程,如果接收到離開訊號,就會攔截並再次估算 Fun,而 Fun 的內容是再呼叫 keep_alive,重新產生一個 Name 的行程。
keep_alive(Name, Fun) ->
register(Name, Pid = spawn(Fun)),
on_exit(Pid, fun(_Why) -> keep_alive(Name, Fun) end).
on_exit(Pid, Fun) ->
spawn(fun() ->
process_flag(trap_exit, true),
link(Pid),
receive
{'EXIT', Pid, Why} ->
Fun(Why)
end
end).
register 跟 on_exit 之間,可能會因為 race condition 而造成問題。當程式裡面使用到 spawn、spawn_link、register 時,要注意會不會遇到 race condition。
OTP 內建了server、supervision tree 的功能,這些功能不會遇到 race condition,所以要盡量改使用 OTP library。
參考
Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World
沒有留言:
張貼留言