erlang : 오류 및 프로세스
- 프로그래밍 언어/erlang
- 2022. 10. 12.
Link
- Link는 두 프로세스를 연결하는 명령어다.
- 두 프로세스가 연결되면 하나의 프로세스가 예상치 못한 throw, Exception이 발생할 경우 연결된 프로세스도 함께 종료된다.
- 이 때, Link는 양방향 연결을 의미한다. A가 죽어도 B에 영향을 주고, B가 죽어도 A에 영향을 주는 것을 의미한다.
Link는 위와 같은 개념을 가진다. 프로세스끼리 Link를 하게 된다면 오류의 전파가 최대한 빨리 중지된다는 관점에서 유용하다. 함께 일하는 프로세스 중에서 일부에서 오류가 발생했다고 가정해보자. 멀쩡한 프로세스들은 문제가 생긴 프로세스들에게 의존하고 있을 수 있다. 이 때 상황을 해결하기 위한 방법은 멀쩡한 프로세스가 망가진 프로세스에게서 의존성을 최대한 없애는 것이다. 얼랭에서는 link를 이용해 에러가 난 프로세스와 관련된 모든 프로세스를 죽이는 방식으로 의존성 제거를 빠르게 도와준다.
%% 프로세스끼리 연결
link/1
%% 프로세스의 연결 해제
unlink/1
위 명령을 이용해서 얼랭 프로세스들을 연결시킬 수 있고, 연결을 해제할 수 있다.
%% linkmon.erl
myproc() ->
timer:sleep(5000),
exit(reason).
%% erlang shell
1> c(linkmon).
{ok,linkmon}
2> spawn(fun linkmon:myproc/0).
<0.52.0>
3> link(spawn(fun linkmon:myproc/0)).
true
** exception error: reason
다음과 같이 실습을 해볼 수 있다.
- 5초 후에 reason과 함께 종료하는 프로세스 A를 생성한다.
- 프로세스 A와 Shell 프로세스를 연결한다. 연결 결과로 true를 반환한다.
- 연결된 후 5초 뒤에 프로세스 A가 죽기 때문에, 쉘 프로세스는 해당 메세지를 받는다.
%% linkmon
chain(0) ->
receive
_ -> ok
after 2000 ->
exit("chain dies here")
end;
chain(N) ->
Pid = spawn(fun() -> chain(N-1) end),
link(Pid),
receive
_ -> ok
end.
show_process([]) -> [];
show_process([H|T]) ->
io:format("~n~p", [H]),
help(T).
%% 쉘 실행
4> c(linkmon).
{ok,linkmon}
5> linkmon:show_process(processes()).
...
<0.73.0>
<0.74.0>
<0.77.0>
6> link(spawn(linkmon, chain, [3])).
true
** exception error: "chain dies here"
7> linkmon:show_process(processes()).
...
<0.73.0>
<0.74.0>
<0.77.0>
<0.103.0>[]
- 위 명령을 이용해서 프로세스의 연쇄적인 종료를 볼 수 있다.
- 쉘 프로세스를 시작으로 3 - 2 - 1 순서대로 프로세스가 link된다.
- 1 프로세스는 2초가 지나면 exit()를 발생시킨다.
- 1 프로세스가 비정상적으로 종료한 후, 2 프로세스, 3 프로세스까지 연쇄적으로 종료가 되는 것을 확인할 수 있다.
- 이 때, 종료된 프로세스는 프로세스 리스트에서 사라진다.
spawn_Link()
Pid = spawn(M, F, A),
link(Pid).
spawn()은 프로세스를 생성하는 것이고, link()는 프로세스를 연결하는 것이다. 예를 들어 위와 같은 작업을 할 때 개발자가 의도한 것은 Pid를 생성하고 Link 하는 것이다. 그렇지만 Pid를 생성하고 그 Pid를 Link 하려는 순간 프로세스가 죽을 수 있다. 이 경우 프로세스는 생성되었으나 연결되지 않았음을 의미한다. 분산 프로그램에서는 항상 이런 일이 발생할 수 있다. 따라서 프로세스의 생성과 연결하는 것을 Atomic 하게 보장하는 것이 필요하다.
spawn_link(Function)
spawn_link(M, F, A)
spawn_link()는 프로세스를 생성하고 연결하는 것을 원자적으로 보장하는 함수다. 따라서 기본적으로 이 함수를 사용하는 것이 좋다.
Trap과 System Process
프로세스끼리 오류를 전파할 때는 Message가 아닌 'Signal'이라는 특수한 유형의 메세지를 사용한다. 종료 시그널은 프로세스에 자동으로 작용해서 필요 시에 프로세스를 종료시켜준다. 그런데 항상 연결된 모든 프로세스가 죽는 것을 원하지 않을 수 있다. 예를 들어서 어떤 프로세스가 죽는다고 종료 신호를 보낸다면, 그 종료 신호를 받고 분석하고 복구하는 형태의 프로세스가 필요할 수 있다. 이런 프로세스를 시스템 프로세스라고 한다.
- 시스템 프로세스는 다른 프로세스가 죽었을 때 보내는 종료 신호를 메세지 박스에 저장할 수 있다.
- 일반 프로세스는 종료 신호를 메세지 박스에 저장할 수 없다.
기본적으로 시스템 프로세스와 일반 프로세스는 다음의 차이를 가진다.
process_flag(trap_exit, true).
시스템 프로세스는 위 명령어를 이용해서 만들 수 있다. 이 명령어를 수행하는 프로세스는 시스템 프로세스가 되는 것이다.
1> process_flag(trap_exit, true).
true
2> spawn_link(fun() -> linkmon:chain(3) end).
<0.89.0>
3> spawn_link(fun() -> linkmon:chain(3) end).
<0.99.0>
4> spawn_link(fun() -> linkmon:chain(3) end).
<0.104.0>
5> spawn_link(fun() -> linkmon:chain(3) end).
<0.109.0>
6> spawn_link(fun() -> linkmon:chain(3) end).
<0.114.0>
7> spawn_link(fun() -> linkmon:chain(3) end).
<0.119.0>
8> flush().
Shell got {'EXIT',<0.89.0>,"chain dies here"}
Shell got {'EXIT',<0.99.0>,"chain dies here"}
Shell got {'EXIT',<0.104.0>,"chain dies here"}
Shell got {'EXIT',<0.109.0>,"chain dies here"}
Shell got {'EXIT',<0.114.0>,"chain dies here"}
Shell got {'EXIT',<0.119.0>,"chain dies here"}
ok
- 시스템 프로세스를 한번 수행해보면 다음과 같다.
- 시스템 프로세스로 만들어주는 순간, 쉘 프로세스는 종료 신호를 메세지 박스에 저장할 수 있게 된다.
- 쉘 프로세스의 메세지 박스에서 종료 신호를 읽어올 수 있게 된다.
각 종료별 Trap / Untrapped의 차이
- ok
- normal
- kill
- kill로 죽는 녀석들은 killed라는 atom을 반환한다.
- Throw
- Error
종료 신호는 다음과 같이 여러 형태로 전달될 수 있다. 기본적으로 ok, normal은 정상적으로 프로세스가 종료된 것으로 이해한다. 따라서 연결된 프로세스가 종료되지는 않는다. 또한, kill은 특별한 종료 신호이기 때문에 killed라는 응답을 받는다. Throw / Error 같은 경우에는 트랩되지 않은 결과와 Trap된 결과를 모두 받아볼 수 있다. 또한 어떤 신호는 트랩되지 않은 종료 신호는 없지만, 트랩된 종료 신호는 존재할 수 있다. 여기서 Trap된 종료신호는 메세지 박스에서 받아보는 녀석을 의미한다.
모니터
link는 양방향으로 연결되는 것을 의미한다. 그런데 양방향 연결보다는 단방향 연결이 필요한 상황도 있다. 이 때 사용할 수 있는 것을 '모니터'라고 한다.
- 모니터는 단방향이다.
- 모니터는 쌓을 수 있음.
모니터는 다음 특징을 가진다. 모니터는 기본적으로 어떤 프로세스가 다른 프로세스에서 무슨 일이 일어나는지를 감시하고, 어떤 일을 하고 싶을 때 주로 사용하는 것이다. 링크과 비교했을 때 모니터의 가장 큰 차이점은 무엇일까?
링크는 하나의 연결이 끊어지면 그와 관련된 모든 프로세스가 끊어지는 것을 의미한다. 즉, 클러스터링에서 제외가 된다. 반면 모니터에서 하나의 프로세스를 감시하는 것은 종료하면 그 프로세스와의 관계만 끊어지는 것을 의미한다. 또한 모니터링 되고 있는 프로세스는 모니터 프로세스의 생사여부를 궁금해하지 않아도 된다. 반면 링크가 되고 있는 프로세스는 다른 프로세스의 죽음에 영향을 받는다는 것이다.
1> erlang:monitor(process, spawn(fun() -> timer:sleep(500) end)).
#Ref<0.0.0.77>
2> flush().
Shell got {'DOWN',#Ref<0.0.0.77>,process,<0.63.0>,normal}
ok
- erlang 모니터는 다음 코드로 선언할 수 있다.
- 모니터는 기본적으로 #Ref를 반환해준다. 이 #Ref는 demonitor() 함수를 이용해서 모니터 상태를 제거할 때 필요하다.
%%%%%%%%%%%%%%% 첫번째, Process 죽이기 전에 demonitor 한 경우
3> {Pid, Ref} = spawn_monitor(fun() -> receive _ -> exit(boom) end end).
{<0.73.0>,#Ref<0.0.0.100>}
4> erlang:demonitor(Ref).
true
5> Pid ! die.
die
6> flush().
ok
%%%%%%%%%%%%%%% 두번째, Process를 죽이고 demonitor 한 경우
7> f().
ok
8> {Pid, Ref} = spawn_monitor(fun() -> receive _ -> exit(boom) end end).
{<0.35.0>,#Ref<0.0.0.35>}
9> Pid ! die.
die
10> erlang:demonitor(Ref, [flush, info]).
false
11> flush().
ok
모니터를 활용해서 위의 두 가지 사례를 비교해볼 수 있다.
- 모니터 관계가 유지되면 모니터링 대상이 죽으면, 모니터 프로세스는 종료 신호를 프로세스에게서 전달 받는다.
- 모니터 관계가 해제된다면 모니터링 대상이 죽어도 왜 죽었는지에 대한 종료 신호를 받지 않는다.
Register Process
기본적으로는 프로세스를 atom과 Binding 해서 사용하는 것이 좋다. 아래를 살펴보자.
%% linkmon
start_critic() ->
spawn(?MODULE, critic, []).
judge(Pid, Band, Album) ->
Pid ! {self(), {Band, Album}},
receive
{Pid, Criticism} -> Criticism
after 2000 ->
timeout
end.
critic() ->
receive
{From, {"Rage Against the Turing Machine", "Unit Testify"}} ->
From ! {self(), "They are great!"};
{From, {"System of a Downtime", "Memoize"}} ->
From ! {self(), "They're not Johnny Crash but they're good."};
{From, {"Johnny Crash", "The Token Ring of Fire"}} ->
From ! {self(), "Simply incredible."};
{From, {_Band, _Album}} ->
From ! {self(), "They are terrible!"}
end,
critic().
1> c(linkmon).
{ok,linkmon}
2> Critic = linkmon:start_critic().
<0.47.0>
3> linkmon:judge(Critic, "Genesis", "The Lambda Lies Down on Broadway").
"They are terrible!"
먼저 다음과 같은 코드가 존재한다고 가정해보자. 위 코드는 밴드 + 앨범을 입력으로 받으면 그것에 대한 평가를 내려주는 프로세스를 생성하고 동작한다. 이 때 한 가지 문제점은 다음에서 발생한다.
- judge()에서 critic에게 메세지를 보냈다.
- critic은 메세지를 응답하기 전에 죽는다. judge는 timeout이 발생하고 응답을 받지 않는다.
즉, 프로세스가 죽었을 때 고가용성을 위해서 새롭게 프로세스를 생성해주는 기능이 필요할 것이다.
start_critic2() ->
spawn(?MODULE, restarter, []).
restarter() ->
process_flag(trap_exit, true),
Pid = spawn_link(?MODULE, critic, []),
register(critic, Pid),
receive
{'EXIT', Pid, normal} -> ok;
{'EXIT', Pid, shutdown} -> ok;
{'EXIT', Pid, _} -> restarter()
end.
judge2(Band, Album) ->
critic ! {self(), {Band, Album}},
Pid = whereis(critic),
receive
{Pid, Msg} -> Msg
end.
critic() ->
receive
{From, {"Rage Against the Turing Machine", "Unit Testify"}} ->
From ! {self(), "They are great!"};
{From, {"System of a Downtime", "Memoize"}} ->
From ! {self(), "They're not Johnny Crash but they're good."};
{From, {"Johnny Crash", "The Token Ring of Fire"}} ->
From ! {self(), "Simply incredible."};
{From, {_Band, _Album}} ->
From ! {self(), "They are terrible!"}
end,
critic().
프로세스가 죽으면 그것을 확인하고 새롭게 만들어주는 프로세스를 생성했다.
- 기본적으로 restarter() 프로세스를 생성해서 사용한다.
- restarter() 프로세스는 시스템 프로세스고, 연결된 프로세스가 죽을 경우 이걸 확인하고 처리해준다.
주의할 점은 프로세스가 죽고 새로 생성하면 Pid에 새로운 값이 바인딩 된다. 따라서 프로세스가 죽은 후에 매번 새로 생성되면 restarter()는 매번 PID를 뱉어주고, 그걸 judge2에 전달해줘야한다. 이런 것들을 해결하기 위해서 프로세스를 특정 atom에 bind하는 방식으로 접근할 수 있다.
위의 예시에서는 생성된 PID를 critic 아톰에 Bind해둔다. (critic 프로세스가 죽으면 바인드 된 것이 풀린다!). 그리고 judge2 에서는 매번 critic에서 Bind된 Pid를 가져온 후 답장을 보내는 형태로 작동한다. 그런데 아직까지도 한 가지 문제점이 존재한다.
- critic ! 메세지
- critic 메세지 수신
- critic 응답
- critic 사망
- critic 재생성 (critic Bind 된 PID 변경됨)
- restarter() 프로세스의 Pid와 critic Bind된 Pid가 다르기 때문에 패턴 매치가 되지 않음.
위와 같은 문제점이 발생한다. 따라서 이런 부분을 해결 해주기 위해서 위의 경우에는 Pid는 항상 변할 수 있는 값이라 생각하고, 불변하지 않는 값을 하나 만들어서 메세지 패턴 매칭에 활용할 수 있다. 이 때, 'Reference'를 만들어서 넘겨주는 방식으로 대응할 수 있다.
judge2(Band, Album) ->
Ref = make_ref(),
critic ! {self(), Ref, {Band, Album}},
receive
{Ref, Criticism} -> Criticism
after 2000 ->
timeout
end.
critic2() ->
receive
{From, Ref, {"Rage Against the Turing Machine", "Unit Testify"}} ->
From ! {Ref, "They are great!"};
{From, Ref, {"System of a Downtime", "Memoize"}} ->
From ! {Ref, "They're not Johnny Crash but they're good."};
{From, Ref, {"Johnny Crash", "The Token Ring of Fire"}} ->
From ! {Ref, "Simply incredible."};
{From, Ref, {_Band, _Album}} ->
From ! {Ref, "They are terrible!"}
end,
critic2().
이 때 동작을 유의해서 살펴보면 다음과 같다.
- judge2를 사용하는 프로세스는 죽지 않는다고 가정한다. 대신 critic PID는 바뀔 수 있다.
- critic PID는 죽어서 바뀌지만, judge2가 죽지 않기 때문에 여기에 Binding 된 Ref 값은 일정하다.
- 만약 critic 비평가가 응답을 보내고, 이걸 judge2가 받기 전에 critic이 죽는다면 critic PID가 바뀌어서 이전에는 judge2에서 PID 패턴 매칭이 안되는 문제가 있었다. 그렇지만 judge2는 REF를 그대로 유지하고 있고, critic도 마지막에 REF를 보내고 죽었기 때문에 정상적으로 메세지를 수신할 수 있다.
그래서 결론은 병렬 프로그래밍을 작성한다면, PID가 바뀔 수 있다는 것을 항상 생각하고 make_ref() 같은 것들을 이용해서 불변하는 값을 전달하는 것이 프로그램 안정성에 도움이 될 수 있다는 것이다.
출처
'프로그래밍 언어 > erlang' 카테고리의 다른 글
erlang 공부 : erlang Shell (0) | 2023.12.10 |
---|---|
erlang 문법 삽질 (0) | 2023.10.10 |
erlang : 재귀 (0) | 2022.09.24 |
erlang 9장 : 병행 프로그램과 오류 (1) | 2022.09.21 |
erlang 8장 : 병행 프로그래밍 (1) | 2022.09.19 |