第九章 并发编程中的错误处理
Table of Contents
第九章 并发编程中的错误处理
Erlang并发编程错误处理设计的三个方面:*链接、退出信号、系统进程*。
9.1 链接进程
Erlang中两个进程存在依赖关系, 则可通过*link(Pid)*的方式为其建立关联。
如果一个进程接收到退出信号, 则其默认处理也将退出, 如果在这个进程中捕获退出信号以进行其它相关操作, 则其为系统进程。
9.2 on_exit处理程序
%% 监控进程退出以执行相关动作的函数 on_exit(Pid, Fun) -> spawn(fun() -> %% 使当前进程成为系统进程 process_flag(trap_exit, true), %% 建立关联 link(Pid), %% 接收退出信号并调用相应的处理函数 receive {'EXIT', Pid, Why} -> Fun(Why) end end).
调用结果:
# 定义一个测试函数 1> F = fun() -> receive 1> X -> list_to_atom(X) 1> end 1> end. #Fun<erl_eval.20.82930912> # 创建一个进程 2> Pid = spawn(F). <0.34.0> # 使用退出监控函数创建进程并与Pid相关联 3> lib_misc:on_exit(Pid, fun(Why) -> io:format(" ~p died with:~p~n", [Pid, Why]) end). <0.38.0> # 向Pid发送不能处理的消息使其崩溃退出 4> Pid ! hello. <0.34.0> died with:{badarg,[{erlang,list_to_atom,[hello],[]}]} hello # 可以看到退出监控函数监控到了进程退出并获取了退出缘由 5> =ERROR REPORT==== 3-Jun-2013::18:50:36 === Error in process <0.34.0> with exit value: {badarg,[{erlang,list_to_atom,[hello],[]}]}
9.3 远程错误处理
在分布式、高并发、容错性等方面都有要求的系统中, 显然在可能出错的业务系统中进行错误处理将会导致系统更加庞杂, 但是借助Erlang可以实现错误处理进程与业务进程分离, 甚至不在同一台机器, 基于Erlang这种特性实现的分布式系统的性能是显而易见的。
9.4 错误处理的细节
Erlang错误处理机制的三种基本概念:
- 链接(link)
通过链接在两个进程之间建立关联, 把一群与某个给定进程进行链接的进程集合称为该进程的链接集。
- 退出信号(exit signal)
进程在消亡时通过退出信号通知它的链接集以执行相应的后续处理工作。在某个进程中也可以显式的向其它进程发送退出信号。
- 系统进程(system process)
普通进程接收到非正常退出信号后会随之退出, 但系统进程会捕获信号以进行相应处理。使用process_flag(trap_exit, true)可以使普通进程转换为系统进程。
进程接收到退出信号后的动作依赖于接收进程的状态及退出信号的值。
捕获状态 | 退出信号 | 动作 |
---|---|---|
true | kill | 消亡, 向链接集广播退出信号killed |
true | X | 将{'EXIT', PID, X}加入到邮箱 |
false | normal | 继续运行, 不做任何事, 屏蔽退出信号 |
false | kill | 消亡, 想链接集广播退出信号killed |
false | X | 消亡, 向链接集广播退出信号X |
9.4.1 捕获退出的编程模式
模式1:我不在乎创建的进程是否崩溃
Pid = spawn(fun() ->... end).
模式2:如果我创建的进程崩溃那么我也自行消亡
Pid = spawn_link(fun() ->... end).
模式3:如果我创建的进程崩溃我需要处理错误
process_flag(trap_exit, true), Pid = spawn_link(fun() ->.. end), ... loop(...). loop(State) -> receive {'EXIT', SomePid, Reason} ->loop(State1); ... end.
9.4.2 捕获退出信号(进阶篇)
-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), sleep(1000), status(b, B), status(c, C). %% 将A转换为系统进程以接收退出信号 a() -> process_flag(trap_exit, true), wait(a). %% 将B转换为系统进程(当Bool为true时), 与A建立关联, 等待接收退出信号 b(A, Bool) -> process_flag(trap_exit, Bool), link(A), wait(b). %% 与B建立关联, 根据start函数中的消息参数执行不同的任务 c(B, M) -> link(B), case M of {die, Reason} -> exit(Reason); {divide, N} -> 1/N, wait(c); normal ->t end. %% 将接收到的任何消息打印输出 wait(Prog) -> receive Any -> io:format("Process ~p received ~p~n", [Prog, Any]), wait(Prog) end. %% 利用receive的超时机制使各个进程的消息有时间输出 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. C正常退出, B不设置为系统进程 # 在函数c(B, M)中对应die的消息将导致进程C退出, 而此时Bool值为false, 即进程B不为系统进程, # 则B进程直接随着C进程退出, A进程为系统进程, 接收退出信号打印输出 # 休眠1秒后输出B进程与C进程的状态 1> edemo1:start(false, {die, abc}). Process a received {'EXIT',<0.39.0>,abc} process b (<0.39.0>) is dead process c (<0.40.0>) is dead ok # 2. C正常退出, 但退出信号为normal, B不设置为系统进程 # 使用normal让进程C退出, 此时B不为系统进程且退出信号为normal, 则B进程屏蔽此信号不做任何处理 # 则A进程(其与B进程关联)没有接收任何信号, 没有输出 # 休眠1秒后输出B进程与C进程的状态 2> edemo1:start(false, {die, normal}). process b (<0.43.0>) is alive process c (<0.44.0>) is dead ok # 3. C异常退出, B不设置为系统进程 # 制造除0错误使进程C退出, B进程因为不是系统进程也将退出, 并把消息广播给A进程 # A进程为系统进程, 接收退出信号打印输出 # 休眠1秒后输出B进程与C进程的状态 3> edemo1:start(false, {dovode, 0}). Process a received {'EXIT',<0.74.0>, {{case_clause,{dovode,0}}, [{edemo1,c,2,[{file,"edemo1.erl"},{line,23}]}]}} =ERROR REPORT==== 6-Jun-2013::15:10:38 === Error in process <0.75.0> with exit value: {{case_clause,{dovode,0}},[{edemo1,c,2,[{file,"edemo1.erl"},{line,23}]}]} process b (<0.74.0>) is dead process c (<0.75.0>) is dead ok # 4. C正常退出, 但退出信号为kill, B不设置为系统进程 # 过程同 1 # 只是退出信号修改为killed 4> edemo1:start(false, {die, kill}). Process a received {'EXIT',<0.78.0>,killed} process b (<0.78.0>) is dead process c (<0.79.0>) is dead ok # 5. C正常退出, B设置为系统进程 # 在函数c(B, M)中对应die的消息将导致进程C退出, 而此时Bool值为true, 即进程B为系统进程, # 则B进程将行使接收退出信号并打印输出的功能 # 休眠1秒后输出B进程与C进程的状态 5> edemo1:start(true, {die, abc}). Process b received {'EXIT',<0.83.0>,abc} process b (<0.82.0>) is alive process c (<0.83.0>) is dead ok # 6. C正常退出, 但退出信号为normal, B设置为系统进程 # 此时B为系统进程, 接收退出信号打印输出 # 休眠1秒后输出B进程与C进程的状态 6> edemo1:start(true, {die, normal}). Process b received {'EXIT',<0.87.0>,normal} process b (<0.86.0>) is alive process c (<0.87.0>) is dead ok # 7. C正常退出, 但退出信号为kill, B设置为系统进程 7> edemo1:start(true, {die, kill}). Process b received {'EXIT',<0.91.0>,kill} process b (<0.90.0>) is alive process c (<0.91.0>) is dead ok
9.5 错误处理原语
spawn_link
spawn_link(Fun) -> Pid
创建进程并与当前进程建立关联。
process_flag
process_flag(trap_exit, true)
将当前进程转换为系统进程
link
link(Pid) -> true
在两个进程之间建立双向的关联。
unlink
unlink(Pid) -> true
移除进程之间的关联。
exit
exit(Why)-> none()
终止当前进程。
exit(Pid, Why) -> true
向指定进程发送退出信号及原因。
monitor
erlang:monitor(process, Item) -> MonitorRef
建立一个针对进程的监视器。
9.6 链接进程集
利用链接进程集的特性可以实现在某个进程终止时同时终止与之相关的所有进程的功能。
9.7 监视器
因为使用link建立的进程间的关联是对称的, 因此如果有建立非对称链接的需求, 则应使用监视器实现。
9.8 存活进程
%% 创建一直存活的进程 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(Name, Pid = spawn(Fun))
语句中可能在创建进程时存在问题, 也可能在注册时存在问题, 而这些都需要监测, 比如在注册之前使用is_process_alive查看进程是否存在, 使用registered查看相同名称是否已经被注册等。