erlang 9장 : 병행 프로그램과 오류
- 프로그래밍 언어/erlang
- 2022. 9. 21.
9.1 프로세스 연결하기
link(Pid)
프로세스와 프로세스를 연결하기 위해서는 link()라는 BIF를 이용해야한다. link를 이용하면 self() 프로세스와 Pid 프로세스는 연결되게 된다. 연결된다는 것은 하나의 프로세스가 죽으면 다른 프로세스는 종료 신호를 받게 되고, 종료 신호를 받은 프로세스는 별다른 조치를 하지 않는 경우 함께 죽게 된다.
프로세스는 종료 신호를 잡도록(Trap) 요청할 수 있고, 종료 신호를 잡아서 처리할 수 있는 프로세스를 시스템 프로세스라고 한다.
9.2 시스템 프로세스 만들기
process_flag(trap_exit, true),
시스템 프로세스는 다른 프로세스의 종료 신호를 받으면, 그 종료 신호를 처리할 수 있는 프로세스다. 시스템 프로세스를 만들기 위해서는 process_flag() 메서드를 이용해야한다. 이 메서드를 이용하면 해당 프로세스는 시스템 프로세스로 바뀐다.
%% a31.erl
on_exit(Pid, Fun) ->
spawn(fun() ->
process_flag(trap_exit, true),
link(Pid),
receive
{'EXIT', Pid, Why} -> Fun(Why)
end
end).
error_func() ->
receive
X -> list_to_atom(X)
end.
- on_exit()는 시스템 프로세스다. 시스템 프로세스가 된 후에 전달받은 프로세스와 연결하고, 종료 신호를 받으면 처리해주는 역할을 한다.
- error_func()는 일반 프로세스를 만들기 위한 함수다.
# 프로세스 만들기
Pid = spawn(fun() -> a31:error_func() end).
# 시스템 프로세스 만들기
a31:on_exit(Pid, fun(Why) ->
io:format("~p died with ~p~n", [Pid, Why]) end).
# 에러 유발
Pid ! abc
>>>>>>>>>>>
<0.98.0> died with {badarg,
[{erlang,list_to_atom,
[qwer],
[{error_info,#{module => erl_erts_errors}}]},
{a31,error_func,0,[{file,"a31.erl"},{line,28}]}]}
=ERROR REPORT==== 20-Sep-2022::11:17:06.859565 ===
Error in process <0.98.0> with exit value:
{badarg,[{erlang,list_to_atom,
[qwer],
[{error_info,#{module => erl_erts_errors}}]},
{a31,error_func,0,[{file,"a31.erl"},{line,28}]}]}
- 다음과 같이 테스트를 해볼 수 있다. 일부러 에러를 유발해보면 on_exit 프로세스가 Pid 프로세스의 종료 신호를 전달 받는다.
- 이 때 패턴 매치(EXIT, Pid, Why) 를 통해서 선택적으로 메세지를 수신받아서, 패턴 매치가 되는 경우 io:format을 통해 메세지를 출력시켜준다 .
- 주의사항
- 이 때, a31:on_exit에서 전달하는 Pid와 format에 전달되는 Pid는 같아야한다.
- 예를 들어 Pid1 / Pid2를 각각 전달했다고 하면 Pid1가 종료되었는데 로그에는 Pid2가 종료되었다고 뜬다.
9.3 오류의 원격 처리
얼랭 시스템은 많은 수의 병렬 프로세스로 구성되어있다. 따라서 오류가 발생한 프로세스에서만 오류를 다루면 안된다. 왜냐하면 다른 프로세스에도 영향을 미칠 수 있기 때문에 다른 프로세스들도 다른 프로세스의 오류를 다를 수 있어야한다. 따라서 얼랭에서는 오류의 원격 처리를 항상 고려해야한다.
9.4 오류 처리 Detail
- 링크
- 링크는 두 프로세스가 연결된 것을 의미한다. 연결되었다는 의미는 한 프로세스가 종료되면 다른 프로세스로 확산될 수 있다는 것을 의미한다.
- 종료 신호
- 프로세스가 죽을 때, 해당 프로세스가 생성하는 신호다. 이 신호는 죽는 프로세스와 연결된 모든 프로세스에게 broadCast 된다.
- exit()를 이용해서 본인은 죽지 않으면서, 다른 프로세스에게 종료 신호를 보낼 수 있음.
- 시스템 프로세스
- 어떤 프로세스가 비정상 종료 신호를 받으면, 프로세스도 죽음. 종료 신호를 받아서 처리하려면 시스템 프로세스가 되어야한다.
- 시스템 프로세스는 종료 신호를 받으면, 종료 신호는 메세지 {'EXIT', Pid, Why}로 변환되서 메일 박스에 저장됨.
오류 처리에는 크게 세 가지가 존재한다. 위의 세 가지를 잘 숙지해야한다.
trap_exit | 종료 신호 | 행동 |
true | kill | 시스템 프로세스가 죽음. 종료 신호 killed를 연결 집합에 전달. |
true | X | 메일 박스에 종료 신호가 추가됨. |
false | normal | 계속함 (아무것도 하지 않고 신호 사라짐) |
false | kill | 죽음 : 종료 신호 killed를 연결 집합에 전달. |
false | X | 죽음 : 종료 신호 X를 연결 집합에 전달 |
종료 신호가 Kill로 오는 경우가 있을 수 있다. 이 Kill 종료 신호는 잡을 수 없는 신호다. 따라서 이 신호를 받은 프로세스는 시스템 프로세스라도 종료된다. 이 프로세스는 죽으면서 Killed라는 종료 신호를 연결 집합에게 전달한다. Kill / Killed는 다른데 Killed를 받은 프로세스는 죽지 않는다. 즉, 하나의 프로세스만 죽이고 이 사실을 다른 프로세스에게 알리고 싶을 때 주로 사용한다고 한다.
종료 잡기 프로세스
1. 자식 프로세스의 종료를 무시함
Pid = spawn(fun() -> ... end)
자식 프로세스가 죽어도 연결되어 있지 않기 때문에 부모 프로세스는 자신의 일을 수행함.
2. 자식 프로세스가 죽으면 같이 죽음
Pid = spawn_link(fun() -> ... end).
- 자식 프로세스를 생성하고, 자식 프로세스는 부모 프로세스와 연결된다.
- 따라서 자식 프로세스가 죽으면 부모 프로세스도 함께 죽는다.
3. 자식 프로세스가 죽으면, 오류를 처리함.
process_flag(exit_trap, true),
Pid = spawn_link(fun() -> ... end),
...
loop(...). %% 루프 실행
loop(State) ->
receive
{'EXIT', Pid, Why} ->
%% 오류로 무언가를 한다
loop(State1);
...
end
- process_flag()를 통해 시스템 프로세스가 된다. 따라서 이제 종료 신호가 오는 경우 프로세스의 메일 박스로 종료 메세지가 전달된다.
- loop를 평가하는 프로세스는 이제 루프를 돌면서 종료 신호를 잡는다.
9.5 오류 처리 함수
- spawn_link(Fun) : 프로세스를 만들고 link()한다. 그렇지만 spawn() + link()가 atomic 하게 이루어진다.
- process_flag(trap_exit, true) : 현재 프로세스를 시스템 프로세스로 만든다.
- link(Pid) : 프로세스끼리 연결시켜준다. 이 때 연결은 대칭적이다.
- unlink(Pid) : Pid와 현재 프로세스의 연결을 제거한다.
- exit(Why) : 현재 프로세스를 사유 Why로 종료시킨다. 현재 프로세스는 종료 신호를 현재 프로세스와 연결된 모든 프로세스에게 broadCast 한다.
- exit(Pid, Why) : 종료 신호를 Why와 함께 Pid로 보낸다.
- erlang:monitor(process, Item) : 모니터를 설정한다. Item은 Pid / 프로세스의 등록된 이름이다.
erlang은 프로세스를 링크하는 여러 함수들이 존재한다. 다음을 사용할 수 있다.
9.6 연결된 프로세스 집합
어떤 계산을 연속적으로 수행하는 병렬 프로세스가 한 무리 있다. 만약 이 때, 하나의 프로세스가 죽었을 때 관련된 모든 프로세스를 없애는 방법은 어떤 것이 있을까? 가장 쉬운 방법은 프로세스가 서로 연결되고, 종료를 잡지 않도록 하는 것이다.
link()로 각 프로세스를 다음과 같이 연결해둔 후, 종료를 잡지 않도록 한다. 위의 예시에서 #1 프로세스가 죽게 되면 연결된 #2,3,4 프로세스가 죽게 된다.
9.7 모니터
link()를 이용하면 연결된 프로세스의 종료가 서로에게 영향을 준다. 그런데 이러고 싶지 않을 경우가 있다. 이 때 모니터를 사용하면 된다. 모니터는 비대칭적인 연결이다. 프로세스 A가 프로세스 B를 모니터하고, B가 죽으면 A는 종료 신호를 받을 것이다. 그렇지만 A가 죽을 경우 B는 종료 신호를 받지 못한다.
9.8 계속 살아 있는 프로세스
어떤 이유로 프로세스가 죽으면, 그 프로세스를 다시 시작하는 프로세스를 만들 수 있다.
on_exit(Pid, Fun) ->
spawn(fun() ->
process_flag(trap_exit, true),
link(Pid),
receive
{'EXIT', Pid, Why} -> Fun(Why)
end
end).
%% PID를 생성하고 등록한다.
keep_alive(Name, Fun) ->
register(Name, Pid = spawn(Fun)),
on_exit(Pid, fun(_Why) -> keep_alive(Name, Fun) end).
- 먼저 Fun을 수행하는 Pid를 생성한 후, Name에 등록한다.
- on_exit() 함수에 생성된 Pid와 Keep_alive를 수행하도록 던져준다.
- on_exit() 함수는 시스템 프로세스로 생성되어서 Pid가 죽는 순간 에러 메세지를 받고, keep_alive 함수를 수행해준다.
- Keep_alive를 수행하던 프로세스가 죽으면 register()는 해제가 된다. 그리고 시스템 프로세스가 새로운 keep_alive()를 호출해주기 떄문에 새로운 함수 스택 영역이 생긴다. 따라서 Pid는 Unbound 된 상태이기 때문에 새로운 프로세스를 생성해서 바인드 해도 문제가 없다.
- 따라서 죽지 않는 프로세스가 만들어진다.
그렇지만 한 가지 맹점이 있다. 이 프로세스는 완벽하게 동작하지는 않는다.
%% PID를 생성하고 등록한다.
keep_alive(Name, Fun) ->
register(Name, Pid = spawn(Fun)),
on_exit(Pid, fun(_Why) -> keep_alive(Name, Fun) end).
예를 들어 register를 수행한 직후에 바로 프로세스가 죽으면 on_exit()는 실행되지 않는다. 따라서 이 경우에는 프로세스가 정상적으로 생성되지 않는다. 어떤 경우에 이런 문제가 발생할까? 동일한 Name에 Pid를 등록하기 위해 여러 프로세스가 동작하는 경우, 문제가 발생한다. Race Condition의 문제다. 따라서 얼랭 코드를 작성할 때는 동시성 문제가 발생하지 않도록 작성해야한다.
'프로그래밍 언어 > erlang' 카테고리의 다른 글
erlang : 오류 및 프로세스 (0) | 2022.10.12 |
---|---|
erlang : 재귀 (0) | 2022.09.24 |
erlang 8장 : 병행 프로그래밍 (1) | 2022.09.19 |
erlang 3장 (0) | 2022.09.17 |
erlang 2장 (0) | 2022.09.06 |