erlang 공부 : Errors and Processes
- 프로그래밍 언어/erlang
- 2023. 12. 10.
참고
Link
- Link는 erlang의 두 프로세스를 연결해주는 기능이다.
- 연결된 두 프로세스 중 하나의 프로세스라도 죽으면, link된 다른 프로세스도 죽는다.
- 한 프로세스는 여러 프로세스와 Link 관계가 될 수 있다
Link 개념은 아래 경우에 굉장히 유용하게 사용할 수 있다.
에러를 가진 프로세스는 죽었지만, Link된 프로세스는 죽지 않는 경우. 그러나 종속성을 가진 모든 프로세스가 죽어야 하는 경우
이런 경우에 종속성을 가진 모든 프로세스를 죽여서 Fast Fail을 달성하기 위해서 굉장히 유용한 개념이다. 이 작업은 Link를 이용해서 달성할 수 있다.
위 그림처럼 얼랭 프로세스들을 Link 시킬 수 있고, 하나의 프로세스라도 죽는다면 모든 프로세스가 죽게 된다. (한 프로세스는 여러 프로세스와 Link 관계가 될 수 있다. 0.150.0처럼)
% 현재 프로세스 - Pid 프로세스를 Link함.
link(Pid).
% 현재 프로세스 - Pid 프로세스의 Link를 제거
unlink(Pid).
erlang에서는 link/1 메서드를 이용해 현재 프로세스와 특정 Pid를 가진 프로세스를 링크해준다. 아래에서 몇 가지 예시를 살펴본다.
첫번째 코드
my_proc() ->
timer:sleep(5000),
exit(reason).
5초를 대기한 후에 exit()로 종료하는 메서드가 있다. 이 메서드를 실행하는 프로세스를 하나 생성하고, eralng 쉘과 link 해보면 어떤 결과가 있을까?
21> self().
<0.160.0>
22> link(spawn(fun link_mon:my_proc/0)).
true
** exception error: reason
23> self().
<0.171.0>
위처럼 얼랭 쉘에서 코드를 실행해보고 결과를 살펴보자.
- 처음에는 쉘 프로세스가 <0.160.0>이다.
- my_proc()을 수행하는 프로세스와 erlang 쉘 프로세스를 link한다.
- 5초 뒤에 종료되며 exception error: reason을 받는다.
- self()를 수행해보면 현재 erlang 쉘 프로세스는 <0.171.0>인 것을 알 수 있다.
이것은 얼랭 쉘이 생성한 my_proc() 프로세스가 죽으면서 얼랭 쉘 프로세스도 함께 죽었다는 것을 의미한다.
두번째 코드
chain(0) ->
receive
_ -> ok
after 2000 ->
exit("chain dies here")
end;
chain(N) ->
Pid = spawn(?MODULE, chain, [N-1]),
link(Pid),
receive
_ -> ok
end.
다음 erlang 코드가 있다고 가정해보자. 이 코드는 다음과 같이 동작한다.
- chain(N) 메서드가 실행될 때 마다 chain(N-1) 프로세스를 생성하고 현재 프로세스와 link()한다.
- chain(0)이 호출되면 2초 기다린 후 "chain dies here" 메세지와 함께 죽는다.
% 함수 실행 전 프로세스 개수 : 43
26> length(processes()).
43
% 프로세스 10개 생성 -> 2초 후, 모든 프로세스 제거됨.
27> link_mon:chain(10).
** exception exit: "chain dies here"
% 함수 실행 후 프로세스 개수 : 43
28> length(processes()).
43
chain() 메서드를 호출해서 10개의 프로세스를 생성했다. 함수 실행 전후의 프로세스 개수를 살펴보면 43개로 동일한 것을 알 수 있다. 이것은 chain(0)가 호출되었을 때 exit()가 발생하고, 이 때문에 link된 모든 프로세스가 정상적으로 종료되었다는 것을 의미한다.
세번째 코드
a1() ->
P1 = spawn(?MODULE, b1, [self()]),
P2 = spawn(?MODULE, c1, [self()]),
io:format("a1: ~p, b1: ~p, c1: ~p ~n", [self(), P1, P2]).
b1(Pid) ->
link(Pid),
timer:sleep(5000),
exit(no_answer).
c1(Pid) ->
link(Pid),
timer:sleep(2000),
io:format("c1 alive~n"),
c1(Pid).
헷갈릴 수 있는 link 개념을 잘 볼 수 있도록 다음 코드를 추가했다.
위 코드를 실행하면 위 그림처럼 프로세스가 서로 관계를 맺게된다. 이 때 b1() 프로세스는 5초 뒤에 죽고, c1() 프로세스는 2초마다 "c1() alive"를 얼랭 쉘에 프린트한다.
% 함수 실행
29> link_mon:a1().
a1: <0.201.0>, b1: <0.204.0>, c1: <0.205.0>
ok
c1 alive
c1 alive
** exception error: no_answer
30>
얼랭 쉘에서 함수를 실행해보면 생각한 것처럼 동작한다.
- a1, b1, c1 프로세스의 ID를 모두 출력한다.
- 5초가 지나는 동안 c1 alive를 두번 프린트한다.
- 5초가 지난 후 no_answer를 이유로 b1 프로세스가 죽는다.
- b1 프로세스가 죽으며 순차적으로 a1, c1 프로세스가 종료된다. 따라서 c1 alive는 출력되지 않는다.
2. It's Trap!
- 프로세스가 죽으면 link된 프로세스도 함께 종료된다. 이 때, 프로세스가 죽으면 link된 프로세스로 'Exit signal'이라는 특수한 타입의 메세지를 보내고, 이 signal 메세지에 의해 link된 프로세스가 종료된다.
- 프로세스가 죽었을 때, link된 프로세스로 {'EXIT', Pid, reason} 메세지를 보낸다. Pid는 죽은 프로세스, reason은 프로세스가 죽은 이유다.
Fast Fail은 프로그램의 안정성을 유지하는 관점에서 좋은 방법이 된다. 그 관점에서 Link된 프로세스끼리 Kill Propagation으로 죽는 것은 훌륭한 Fast Fail 전략이 된다. 여기서 중요한 부분은 Fast Fail로 프로세스가 죽고, 그 프로세스를 다시 살리는 부분이다.
2.1 시스템 프로세스
프로세스가 죽은 것을 감지하고 다시 살리기 위해서 우리는 특정 프로세스를 시스템 프로세스로 만들 수 있다. 시스템 프로세스는 다음 특성을 가진다.
- 시스템 프로세스는 exit signal을 일반적인 메세지로 변환할 수 있다.
- exit signal은 일반 메세지로 변환되어 시스템 프로세스 메세지함에 저장됨.
- receive절을 이용해서 읽어올 수 있고, 안 읽어와도 시스템 프로세스는 종료되지 않음.
- 나머지는 일반 프로세스와 동일하다.
% 시스템 프로세스로 만들기
process_flag(trap_exit, true).
특정 프로세스를 시스템 프로세스로 만들려면 다음 코드를 사용하기만 하면 된다.
2.1.1시스템 프로세스 코드 실행 결과 살펴보기
% spawn_link() : Blocking
% spawn() : Non Blocking
system_process(N) ->
process_flag(trap_exit, true),
Pid = spawn(?MODULE, chain, [N]),
link(Pid),
receive
X -> X
end.
chain(0) ->
io:format("it will be died~n"),
receive
_ -> ok
after 2000 ->
exit("crash by me. ~n", reason)
end;
chain(N) ->
Pid = spawn(fun() -> chain(N-1) end),
link(Pid),
io:format("new Process created. Pid : ~p~n", [Pid]),
receive
_ -> ok
end.
system_process() 함수는 시스템 프로세스가 되어서 에러 메세지를 변환하는 작업까지 해준다.
% 현재 쉘 프로세스 PID 확인
35> self().
<0.457.0>
% 시스템 프로세스 생성 및 동작
36> spawn_link(e_trap, system_process, [5]).
new Process created. Pid : <0.565.0>
...
it will be died
=ERROR REPORT==== 11-Dec-2023::13:15:23.859000 ===
Error in process <0.569.0> with exit value:
...
% 현재 쉘 프로세스 PID 확인
37> self().
<0.457.0>
- 현재 쉘 프로세스와 system_process()를 link한다. 따라서 시스템 프로세스가 죽으면 쉘 프로세스가 종료되며, 쉘 프로세스의 PID가 바뀔 것이다.
- spawn_link() 전후로 쉘 프로세스의 PID는 바뀌지 않았다. 이것은 시스템 프로세스가 자식 프로세스의 exit signal을 Convert해서 메세지함에 Catch한 것을 의미한다.
2.1.2 테스트 시, 주의 사항
주의해야할 부분은 spawn(), spawn_link()는 서로 다른 형태로 동작한다는 것이다.
- spawn(): Link까지 Atomic하게 처리해주지는 않지만 spawn()의 결과를 기다리지 않고 다음 코드 블럭으로 넘어감. (Blocking)
- spawn_link()는 spawn_link()의 결과가 완료될 때까지 기다림. (Non-Blocking)
2.2 프로세스를 종료시키는 다양한 방법
다음 명령어를 이용해서 프로세스를 종료시켜 볼 수 있다.
% normal로 종료 -> 정상 종료됨.
spawn_link(fun() -> ok end)
spawn_link(fun() -> exit(normal) end)
% reason으로 종료됨. -> 비정상 종료
spawn_link(fun() -> exit(reason) end)
% badarith로 종료됨. -> 비정상 종료
spawn_link(fun() -> 1/0 end)
% reason으로 종료 -> 비정상 종료
spawn_link(fun() -> erlang:error(reason) end)
% nocath로 종료됨. -> 비정상 종료
spawn_link(fun() -> throw(rocks) end)
기존에 존재하는 프로세스를 존재하는 케이스는 다음과 같다.
- 정상 종료 : normal로 종료하는 것임. 일반적으로 함수의 실행이 끝나서 종료되면 normal로 종료됨.
- 비정상 종료 : 그 외의 이유로 종료되는 것은 비정상 종료임. 시스템 프로세스가 아니라면 에러가 전파됨.
2.2.1 exit/2를 이용해 다른 프로세스 종료시키기
exit/2 메서드는 다른 얼랭 프로세스를 종료시킬 수 있는 '총'같은 메서드다.
%
exit(Pid, Reason).
% 정상 종료
exit(self(), normal).
exit(spawn_link(fun() -> timer:sleep(50000) end), normal
% 비정상 종료
exit(spawn_link(fun() -> timer:sleep(50000) end), reason)
위 명령어를 이용하면 관련된 프로세스를 종료 시킬 수 있다.
exit/2 메서드가 다른 프로세스의 종료를 보장하지는 않는다.
exit/2 메서드는 다른 프로세스에게 'exit signals'를 보내는 방식으로 동작한다. 그런데 다른 프로세스가 시스템 프로세스인 경우, exit/2 메서드를 이용해 보낸 exit signals은 프로세스의 메세지함에 저장된다. 따라서 이런 경우에는 프로세스가 종료되지 않는다.
2.2.2 Kill로 종료
앞서 이야기한 것처럼 exit/2 메서드는 프로세스의 종료를 보장하지는 않는다. exit/2 메서드는 종료 신호를 보내는 형태로 동작하는데, 시스템 프로세스는 종료 신호를 일반 메세지로 변환해서 보관하기 때문이다. 따라서 시스템 프로세스에게 아무리 종료 신호를 보내더라도 시스템 프로세스의 메세지 함에 무의미한 종료 신호만 쌓이게 된다. 만약 프로세스를 반드시 종료해야하는 상황이라면 'kill'을 함께 사용하면 된다.
- 일반적인 종료 신호는 시스템 프로세스가 컨버팅 할 수 있음.
- Kill은 절대로 컨버팅 할 수 없는 종료 신호다.
% Kill로 프로세스 종료.
exit(spawn_link(fun() -> timer:sleep(50000) end), kill)
>> ** exception exit: killed
exit(self(), kill)
>> ** exception exit: killed
spawn_link(fun() -> exit(kill) end)
** exception exit: killed
kill 명령어로 특정 프로세스의 종료를 보장할 수 있다. 한 가지 특이한 점은 종료 이유로 exception exit: killed 형태가 보여진다는 것이다. 만약 my_reason이라는 이유로 exit()을 실행했다면, exception exit: my_reason이 된다. 그런데 kill만 killed로 바뀌는 이유는 무엇일까?
만약 종료 이유가 kill로 그대로 Link된 프로세스로 전파된다면, 시스템 프로세스라도 종료 신호를 일반 메세지로 컨버팅 할 수 없기 때문에 Link 된 모든 프로세스가 종료된다. 이런 동작은 문제가 있을 수 있기 때문에 kill로 특정 프로세스가 종료되더라도 실제로 exit signal를 전파할 때는 "killed"로 바꾼다. 이처럼 kill → killed로 이름이 변경되기 때문에 자식 프로세스가 종료되어도 시스템 프로세스는 그 에러 메세지를 캐치해서 다시 한번 자식 프로세스를 살릴 수 있게 동작한다.
3. Monitors
- 양방향 연결임.
- 한 프로세스의 종료가 다른 프로세스에게도 전파됨.
- 프로세스끼리는 One To One 링크만 가질 수 있음.
- A 프로세스는 각 프로세스에 대한 링크를 가질 수 있음.
- A 프로세스와 각 프로세스 사이에는 단 하나의 링크만 존재함. (A-B 링크는 0, 1중 하나)
- 주로 Server ↔ Worker 간의 관계일 때 사용한다.
Link는 다음 특성을 가진다. 그런데 다른 프로세스가 어떤 상태인지만 알고 싶을 때 굳이 '양방향 연결'이 필요할까? 그렇지는 않다. 또한 단순히 '감시'하는 프로세스가 죽었을 때, 감시 당하던 프로세스까지 종료시키는데 타당하지 않을 수 있다. 만약 이런 경우를 피하고 싶다면, erlang의 Monitor를 사용해 볼 수 있다.
3.1 Monitor의 특성
- 단방향 감시 : Monitor는 한 프로세스가 다른 프로세스를 단방향으로 감시함.
- 감시하는 프로세스가 죽어도 감시받는 프로세스에게 영향 주지 않음.
- 감시받는 프로세스가 죽으면 감시하는 프로세스가 메세지를 받음.
- 동일한 프로세스에 여러 개의 모니터를 생성할 수 있음.
erlang의 모니터는 다음 특성을 가진다. erlang에서 모니터를 생성하는 방법은 다음과 같다.
% monitor 생성
1> erlang:monitor(process, spawn(fun() -> timer:sleep(500) end)).
> #Ref<0.1093098695.3203661825.197446>
% 종료 메세지 확인
2> flush().
Shell got {'DOWN',#Ref<0.1093098695.3203661825.197446>,process,<0.659.0>,
normal}
- erlang:monitor()를 이용해서 모니터를 생성할 수 있음.
- monitor 함수 안에는 process, port, time_offset이 들어갈 수 있음. (https://www.erlang.org/doc/man/erlang#monitor-2)
- monitor의 실행 결과로 해당 모니터의 참조값(Reference)가 반환된다.
모니터를 생성했을 때 반환된 모니터 참조값은 모니터를 해제할 때 사용할 수 있다. 또한 위 코드는 0.5초 이후에 spawn된 프로세스가 정상적으로 종료되는데, 모니터를 건 프로세스의 메세지함에 '프로세스 종료'에 대한 메세지가 전송되어 있는 것을 확인할 수 있다.
3.2. Monitor는 Stackable함.
모니터는 쌓을 수 있다. A 프로세스, B 프로세스가 있다면 A-B 프로세스 사이에 여러 개의 모니터를 생성할 수 있다는 것을 의미한다. 여러 모니터를 생성해서 각각의 용도로 모니터를 사용하는 방법도 있다.
72> erlang:monitor(process, Pid).
#Ref<0.1093098695.3203661829.196950>
73> erlang:monitor(process, Pid).
#Ref<0.1093098695.3203661829.196957>
74> erlang:monitor(process, Pid).
#Ref<0.1093098695.3203661829.196964>
75> erlang:monitor(process, Pid).
#Ref<0.1093098695.3203661829.196971>
76> erlang:monitor(process, Pid).
#Ref<0.1093098695.3203661829.196978>
이처럼 동일 프로세스에 대해 여러 모니터를 생성할 수 있음을 알 수 있다.
3.3 Monitor 해제하기
% 모니터 생성하기
77> REF = erlang:monitor(process, Pid).
#Ref<0.1093098695.3203661829.196994>
% 모니터 제거하기
78> erlang:demonitor(REF).
true
monitor를 해제할 때는 demonitor() 메서드를 이용하면 된다.
4. Naming Process
Erlang은 프로세스에 고유한 ID인 PID를 붙여주고, 프로세스끼리는 PID를 이용해 통신할 수 있다. 이번에 프로세스 종료 시, 재시작해주는 Restarter를 공부했었는데 그 개념과 함께 PID만 사용했을 때의 부족한 부분을 살펴보자.
4.1.1 Step1
% 이 경우 critic 프로세스가 죽었을 때, 살려주는 녀석이 없음.
start_critic() ->
spawn(?MODULE, critic, []).
judge(Pid, Band, Album) ->
Pid ! {self(), {Band, Album}},
receive
{_From, Msg} -> Msg
after 2000 ->
timeout
end.
critic() ->
receive
{From, {"RATM", "A"}} ->
From ! {self(), "They are great!"};
{From, {"Nirvana", "B"}} ->
From ! {self(), "They are good!"};
{From, {_Band, _Album}} ->
From ! {self(), "is it best?"}
end.
가장 기초가 되는 코드다.
- critic() 메서드를 수행하는 프로세스를 생성한다.
- 클라이언트는 critic 프로세스에게 judeg() 메서드를 이용해 밴드 + 앨범에 대한 평가를 부탁한다.
이 코드에는 어떤 문제점이 있을까? 바로 critic 프로세스가 죽었을 때, 살려주는 녀석이 없다는 것이다.
29> P_step1 = e_naming_process_step1:start_critic().
<0.847.0>
32> e_naming_process_step1:judge(P_step1, "hello", "ballo").
"is it best?"
33> exit(P_step1, no_answer).
true
34> e_naming_process_step1:judge(P_step1, "hello", "ballo").
timeout
얼랭 쉘에서 critic 프로세스를 종료한 후, 다시 메세지를 보내보면 timeout이 발생하는 것을 확인할 수 있다. critic 프로세스를 다시 살려주는 녀석이 없기 때문에 발생하는 문제다.
4.1.2 Step2.
start_critic() ->
spawn(?MODULE, restarter, []).
restarter() ->
process_flag(trap_exit, true),
spawn_link(?MODULE, critic, []),
receive
{'EXIT', Pid, normal} -> ok;
{'EXIT', Pid, shutdown} -> ok;
{'EXIT', Pid, _} -> restarter()
end.
judge(Pid, Band, Album) ->
Pid ! {self(), {Band, Album}},
receive
{_From, Msg} -> Msg
after 2000 ->
timeout
end.
critic() ->
receive
{From, {"RATM", "A"}} ->
From ! {self(), "They are great!"};
{From, {"Nirvana", "B"}} ->
From ! {self(), "They are good!"};
{From, {_Band, _Album}} ->
From ! {self(), "is it best?"}
end.
- restarter() 함수를 시스템 프로세스로 만들고, critic 프로세스가 죽을 때마다 다시 살려주도록 코드를 작성했다.
- 여기서는 다시 살아난 critic 프로세스의 Pid를 알 수 없다는 문제가 발생한다.
왜냐하면 프로세스를 새로 생성할 때마다 PID가 바뀌기 때문이다. 재생성된 프로세스에도 적절히 접근할 수 있어야 한다.
4.1.3. Step3.
erlang에서는 이 문제를 해결하기 위해 atom에 PID를 등록해서 atom으로 PID에 접근하는 방법을 제공해준다.
- erlang:register(atom, PID)
- atom에 PID를 등록해준다. atom ! MSG 같은 것이 가능해짐.
- 프로세스가 죽으면 등록된 PID가 atom에서 제거됨.
- whereis(atom)
- atom에 등록된 PID를 반환함.
start_critic() ->
spawn(?MODULE, restarter, []).
restarter() ->
process_flag(trap_exit, true),
erlang:register(critic, spawn_link(?MODULE, critic, [])),
receive
{'EXIT', Pid, normal} -> ok;
{'EXIT', Pid, shutdown} -> ok;
{'EXIT', Pid, _} -> restarter()
end.
judge(Band, Album) ->
critic ! {self(), {Band, Album}},
Pid = whereis(critic),
receive
{Pid, Msg} -> Msg
after 2000 ->
timeout
end.
critic() ->
receive
{From, {"RATM", "A"}} ->
From ! {self(), "They are great!"};
{From, {"Nirvana", "B"}} ->
From ! {self(), "They are good!"};
{From, {_Band, _Album}} ->
From ! {self(), "is it best?"}
end.
- 위 코드에서는 restarter가 critic 프로세스를 새로 살릴 때 마다 critic atom에 새롭게 생성된 Pid를 저장한다.
- judge() 메서드를 호출할 때 마다 critic atom에게 메세지를 보낸다.
그런데 이 때 한 가지 동시성 문제가 발생할 수 있는 경우가 있다.
judge(Band, Album) ->
critic ! {self(), {Band, Album}},
Pid = whereis(critic),
receive
{Pid, Msg} -> Msg
after 2000 ->
timeout
end.
동시성 문제가 발생할 수 있는 것은 이 부분이다.
- critic의 Pid = <0,1,0>일 때 judge()를 통해 메세지를 보냄.
- ciritic 프로세스가 죽어서 Restarter가 새로 살림. 이 때 critic의 Pid = <0,50,0>이 됨.
- Pid = whereis(critic)에서 <0,50,0>이 반환됨.
- judge() 메서드는 패턴 매칭에 의해 <0,50,0>에게 메세지를 받을 때까지 기다림.
이런 형태로 코드를 작성하면 동시성 문제가 발생하고, 경우에 따라서 프로그램이 망가질 수 있다. 이 때의 해결방법은 1~2번째 라인에서 Pid가 달라질 수 있다는 것을 미리 파악하고 그에 대해서 코드로 조치하는 것이다.
4.1.4. Step4.
이런 동시성 문제를 해결하면서 선택적 수신을 하기위해서 Reference를 생성해서 전송하는 형태로 처리해 볼 수 있다.
judge(Band, Album) ->
Ref = make_ref(),
critic ! {self(), Ref, {Band, Album}},
receive
{Ref, Msg} -> Msg
after 2000 ->
timeout
end.
critic() ->
receive
{From, Ref, {"RATM", "A"}} ->
From ! {Ref, "They are great!"};
{From, Ref, {"Nirvana", "B"}} ->
From ! {Ref, "They are good!"};
{From, Ref, {_Band, _Album}} ->
From ! {Ref, "is it best?"}
end.
- make_ref()를 이용해 유일한 참조를 생성해서 critic 프로세스에게 요청을 보낼 때 전송한다.
- critic() 프로세스는 메세지를 응답할 때, Ref를 포함해서 보낸다.
- judge() 메서드는 메세지를 receive 할 때 선택적 수신을 위해 Ref에 패턴매칭한다.
이렇게 코드를 작성하면 이전에 발생했던 동시성 문제로부터 자유로울 수 있게 된다.
4.2 프로세스 Naming하기
앞서서 erlang:register()를 이용하면 프로세스에 이름을 붙이는 것이 가능하다는 것을 알게 되었다. 그런데 atom은 한정되어 있으므로 모든 프로세스에 이름을 붙일 수 없다. 이를 고려하면 어떤 규칙으로 프로세스에게 이름을 붙여야 할까?
- VM 내에서 유일하고 중요한 프로세스에는 이름을 붙인다.
- 어플리케이션이 실행되는동안 유지되어야하는 프로세스에는 이름을 붙인다.
- 동적으로 프로세스에 이름을 붙이면 안된다. (폭발 범위가 어디까지인지 알 수 없다)
'프로그래밍 언어 > erlang' 카테고리의 다른 글
erlang 공부 : Designing a Concurrent Application (0) | 2023.12.13 |
---|---|
erlang 공부 : A Short Visit to Common Data Structures (0) | 2023.12.11 |
erlang 공부 : more on multiprocessing (1) | 2023.12.10 |
erlang 공부 : erlang Shell (0) | 2023.12.10 |
erlang 문법 삽질 (0) | 2023.10.10 |