erlang 8장 : 병행 프로그래밍

    프로세스

    프로세스라는 것은 운영체제에서 자주 사용되는 용어이다. 그렇지만 얼랭에서 프로세스는 운영체제가 아닌 프로그래밍 언어에 속한다. 따라서 얼랭에서의 프로세스는 운영체제에서 가지고 있는 프로세스의 느낌과는 조금은 다르다.

    • 프로세스의 생성과 제거가 매우 빠르다.
    • 프로세스 간 메세지 전송이 매우 빠르다.
    • 매우 많은 수의 프로세스를 가질 수 있음.
    • 모든 OS에서 프로세스가 동일한 방식으로 동작한다.
    • 프로세스는 메모리를 공유하지 않고 독립적이다. 
    • 프로세스가 상호작용하는 유일한 방법은 메세지 전달을 통해서다.

    기본적으로 프로세스를 생성하거나 메세지를 전달하는 것은 커널을 통해서 하기 때문에 느린 것으로 인지된다. 그렇지만 얼랭에서의 프로세스는 프로그래밍 언어에 속하기 때문에 일반적으로 알고 있던 프로세스보다 가볍고, 동작이 빠르다는 것으로 이해를 할 수 있다.

    프로세스끼리는 메세지를 보낼 수 있다. 그런데 메세지를 정상적으로 받았는지를 알 수 있는 유일한 방법은 메세지를 받은 프로세스가 "응답"을 해야하는 것이다. 따라서 요청 - 응답 구조로 가급적이면 코드가 구성되는 것이 권장된다.

     

    8.1 병행성 지시어

    Pid = spwan(Fun)

    %% 루프 함수
    loop() ->
      receive
        {rectangle, W, H} ->
          io:format("W*H ~p~n", [W*H]),
          loop();
        {circle, R} ->
          io:format("R*R ~p~n", [3.14*R*R]),
          loop()
      end.
    
    
    %% 인자가 없는 함수의 경우
    start() ->
      spawn(fun loop/0).
    
    
    %% 인자가 있거나 없는 함수의 경우
    start2() ->
      spawn(fun() -> loop() end).
    • Fun을 평가하는 새 자식 프로세스를 생성한다. 새롭게 생성된 프로세스는 호출자와 병렬로 실행된다. 그리고 spawn 명령어를 사용하면 생성된 프로세스의 Pid를 반환해준다. 반환받은 Pid는 메세지를 전달할 때 사용할 수 있다. 
    • 이 때 Fun은 여기서 fun 키워드로 사용할 수도 있고, fun()으로 사용할 수도 있다. 전자는 인자가 없는 함수에 사용이 가능한 것으로 보이고, 후자는 인자가 없는 함수에도 사용할 수 있는 것으로 보인다. 

     

    Process는 병렬로 실행됨.

    G = fun(X) -> time:sleep(1000), io:format("~p~n", [X]) end.
    [spawn(fun() -> G(X) end) || X <- lists:seq(1,10)].
    
    >>>>>>>>>>>>>>>>>>>>>>
    [<0.104.0>,<0.105.0>,<0.106.0>,<0.107.0>,<0.108.0>,
     <0.109.0>,<0.110.0>,<0.111.0>,<0.112.0>,<0.113.0>]
    1  
    2  
    3  
    10 
    4  
    5  
    6  
    7  
    8  
    9

    Process가 실행되도록 다음과 같이 작성해볼 수 있다. 프로세스를 생성하는데, 이 프로세스는 1초간 쉰 다음에 자신이 받은 X값을 출력한다.  이 때, List Comprehension으로 만들어서 순서대로 출력될 것을 기대하지만 실제로는 그렇게 나오지 않는다. 이것은 프로세스가 동시에 실행되기 때문에 이벤트 순서가 더 이상 보장되지 않는다는 것을 의미한다. 

     

    쉘 자체도 일반 프로세스다.

    self().
    <0.41.0>

    쉘 자체도 일반 프로세스로 구현되어있다. self()라는 함수는 현재 사용하고 있는 프로세스의 PID를 반환해준다. 쉘에서 self()를 사용하면 PID가 나오는데, 이것은 쉘 자체가 프로세스로 구현되어있는 것을 의미한다. 

     

    Pid ! Message

    %% Pid1에게만 메세지 보내기
    Pid1 ! M
    
    %% Pid1, Pid2에게 메세지 보내기
    Pid1 ! Pid2 ! ... ! M
    • spwan이 생성되면서 전달받은 Pid를 식별자로 사용해서 특정 프로세스에 원하는 메세지를 보낼 수 있게 된다. 
    • 메세지는 비동기로 전송된다. 즉, 메세지를 전송하는 호출자는 메세지에 대한 응답을 기다리지 않고(Non Blocking) 다음 작업을 수행한다. 
    self() ! self() ! self() ! self() ! double
    >>> double
    
    flush().
    Shell got double
    Shell got double
    Shell got double
    Shell got double
    ok
    • 스스로에게 메세지를 4번 보내봤다. 그리고 메세지가 정상적으로 프로세스의 메일 박스에 도착했는지를 확인했다. 
    • 메일 박스의 메세지를 모두 버리는 것은 flush()를 이용한다. flush()를 이용하면 현재 받은 메세지를 모두 출력한 뒤 메일 박스를 비워버린다.
    • 정상적으로 메세지가 4번 도착한 것을 확인할 수 있다.

     

    received... after... end

    receive
    	Pattern1 [when Guard] ->  Expression1;
        Pattern2 [when Guard] ->  Expression2;
    	Pattern3 -> Expression3
    after Time ->
    	Expression1;
        ...
    end.
    • 프로세스로 전송된 메세지를 받는다.  프로세스로 전송된 메세지는 모두 메일 박스에 저장된다.
    • receive를 할 때 마다 다음 과정을 수행한다.
      1. 만약 타이머가 셋팅 되었다면 타이머가 시작된다. 그리고 프로세스의 메일 박스에서 첫번째 메세지를 가져와서 패턴 매치한다. 패턴 매치가 성공하면 해당 receive는 종료된다.
      2. 패턴 매치가 성공하지 않을 경우, 첫번째 메세지를 save que에 넣는다. 메일 박스에서 두번째 메세지를 가져와서 패턴 매치를 수행한다. 
      3. 위 과정을 반복하며 더 이상 읽어올 메세지가 없는 경우 프로세스는 해당 작업을 멈춘 후, 새 메세지를 기다리도록 리스케쥴 된다. 
      4. 리스케쥴 되어 새로운 메세지가 들어오면 패턴 매치를 다시 시작한다. 그렇지만 이 때, save que에 있는 메세지는 패턴 매치 대상이 아니다. 
      5. 만약 이 때 패턴 매치가 된다면, 타이머는 초기화 되고 save que에 있는 메세지는 다시 메일 박스로 전달된다. 

     

    8.2 ~ 8.3 클라이언트 - 서버

    spwan()을 이용하면 자식 프로세스가 만들어지고, 그 프로세스의 고유 식별자인 Pid가 만들어진다. 부모 프로세스는 자식 프로세스에게 메세지를 보낼 때 Pid를 이용해서 보냈다. 자식 프로세스가 부모 프로세스에게 받은 메세지를 이용해서 작업을 한 후에 부모 프로세스에게 응답을 하고 싶다. 이럴 때, 자식 프로세스는 부모 프로세스에게 메세지를 전달하기 위해 부모 프로세스의 고유한 ID를 알아야한다. 따라서 부모 프로세스는 자식 프로세스에게 메세지를 보낼 때, 자신의 PID를 함께 넘겨줘야한다. 

    Pid ! {self(), Message}

    이 때, 자신의 PID를 넘겨주는 예약어로 self()라는 문법을 사용할 수 있다. self는 현재 프로세스의 PID를 넘겨준다. 따라서 메세지를 보낼 때 위와 같은 형태로 작성해볼 수 있다. 자식 프로세스는 부모의 PID를 받았기 때문에 메세지 처리 결과를 응답할 수 있다. 

    rpc(Pid, Message) ->
      Pid ! {self(), Message},
      receive
        Response -> Response
      end.

    요청을 보낼 때는 다음과 같이 새로운 함수를 하나 만드는 것이 좋다. 함수는 메세지를 수신할 Pid와 보낼 메세지를 전달 받는다. 그리고 메세지를 보낼 때, self()를 이용해서 자신의 Pid와 메세지를 함께 보내준다. 부모 프로세스 또한 자식 프로세스가 보내는 메세지를 들을 준비가 되어 있어야 한다. 따라서 received를 이용해서 메세지를 받을 준비를 한다. 

    %% API
    -export([start/0, rpc/2]).
    
    start() ->
      spawn(fun() -> loop() end).
    
    rpc(Pid, Message) ->
      Pid ! {self(), Message},
      receive
        Response -> Response
      end.
    
    loop() ->
      receive
        {Pid, {rectangle, W, H}} ->
          Pid ! W * H,
          loop();
        {Pid, {circle, R}} ->
          Pid ! R * R * 3.14,
          loop();
        _Other ->
          loop()
      end.

    위와 같은 형태를 이용했을 때, 가장 원시적으로는 다음과 같은 함수를 구성해볼 수 있다. 그런데 이 때 한 가지 문제가 존재한다. rpc()에 문제가 있다. rpc()는 현재 recieve를 하고 있는데 어떤 프로세스든 rpc()에게 메세지를 보낼 수 있다. 우리가 보낸 메세지의 응답이 오기 전에 먼저 다른 프로세스에서 메세지를 보낸다면, 원하지 않는 결과가 발생할 수 있다. 따라서 rpc는 특정 메세지만 선택적으로 받을 수 있도록 개선이 필요하다. 

    start() ->
      spawn(fun() -> loop() end).
    
    rpc(Pid, Message) ->
      Pid ! {self(), Message},
      receive
        {Pid, Response} ->
          Response
      end.
    
    loop() ->
      receive
        {Pid, {rectangle, W, H}} ->
          Pid ! {self(), W * H},
          loop();
        {Pid, {circle, R}} ->
          Pid ! {self(), R * R * 3.14},
          loop();
        _Other ->
          loop()
      end.

    따라서 프로세스와 프로세스가 메세지를 서로에게 주고 받고 있다는 것을 확신하기 위해서 서로의 Pid를 넘겨주는 방식이 필요하다. 이 때, rpc()에는 패턴 매칭이 사용된다. 처음 rpc가 호출되면 rpc의 scope에서는 Pid는 처음으로 특정 값으로 바인딩 된다. 그리고 receive에서 {Pid, Response}를 하게 되는데, 이 때 전달되는 메세지와 기존에 바인드 된 Pid 값과 패턴 매칭이 되는지를 확인한다. 따라서 내가 보낸 곳에서 제대로 받았는지를 확인한 후 메세지를 받을 수 있게 된다. 이를 위해서 자식 프로세스는 자신의 PID를 self()를 이용해서 보내줘야한다. 

     

    8.5 타임 아웃이 있는 Recieve

    어떤 프로세스는 receive를 하지만 그 프로세스에는 영원히 메세지가 오지 않을 수 있다. 따라서 어느 정도 메세지를 기다렸을 때, 더 이상 메세지를 기다리지 않게 하는 방법이 필요하다. 이 때 receive 문에 타임 아웃 기능을 추가할 수 있다. 타임 아웃은 after Time -> Expression으로 구현할 수 있다. 

    <0123.12312581239.2358234> ! {self(), Hello}
    recieve
    	_ -> {ok, hello}
    end

    예를 들어서 잘못된 프로세스에게 메세지를 보냈고, 그 프로세스에게 응답이 올 때까지 기다린다고 가정해보자. 그런데 그 프로세스는 이 세상에 존재하지 않는다고 해보자. 아무튼 메세지를 보낸 프로세스는 receive 절에서 block되어서 기다린다.  이 때 이것은 오류가 아니다. 왜냐하면 정상적으로 메세지를 receive 하기 위해 동자하고 있는 것이기 때문이다. 따라서 이 부분의 해결이 필요하기 때문에 타임 아웃을 넣어줄 필요가 있다. 

    receive
    	Pattern1 [when Guard1] -> 
        	Expressions1;
        Pattern2 [when Guard2] -> 
        	Expressions2;
    after Time ->
    	Expressions
    end.

    after Time을 넣으면 receive 문으로 진입 후 Time만큼의 시간(ms)내에 메세지가 도착하지 않으면 프로세스는 메세지를 기다리지 않고 after 이후의 값을 실행한다. 

     

    타임아웃만 있는 recieve

    sleep(T) ->
    	receive
        after Time ->
        	true
        end.

    타임아웃만으로 구성된 recieve를 만들 수 있다. 이것을 이용해서 현재 프로세스를 Time ms만큼 중단시키는 sleep()함수를 정의할 수 있다. 

     

    타임아웃 값이 0인 receive -> 메세지 flusher

    flush_buffer() -> 
      receive
        _Any -> 
          flush_buffer()
      after 0 ->
        true    
      end.

    타임아웃 값이 0이면 타임 아웃의 본문이 즉각 실행된다. 그렇지만 타임 아웃의 본문이 실행되기 전에 메일 박스에서 아무 패턴이건 매치를 시도한다. 이를 이용하면 어떤 프로세스의 메일 박스를 완전히 비울 수 있다.

    첫번째 메세지를 메일박스에 꺼내서 패턴 매칭을 하고 다음 flush_buffer()를 호출한다. 다시 다음 메세지를 메일박스에서 꺼내서 패턴 매칭을 한다. 이렇게 모든 메세지를 메일박스에서 소모하면 after 이후의 함수가 호출된다. 

    타임아웃 값이 0인 receive -> 특정 메세지를 먼저받기

    pri_recive() ->
      receive
        {alarm, X} ->
          {alarm, X}
      after 0 ->
        receive
          Any ->
            Any
        end
      end.

    receive 문에 들어오면 메일 박스에 있는 모든 메세지를 차례대로 꺼내서  {alarm, X}에 매칭되는지를 확인한다. 만약에 매칭되는 경우가 없다면 after 0 ->이 바로 실행되어서 메일 박스에 있는 가장 첫번째 메세지가 꺼내지게 된다. 이런 로직을 통해서 특정 패턴에 매칭되는 메세지를 가장 최우선으로 받고 그 다음 메세지를 받도록 한다. 

    타이머 만들기

    cancel(Pid) -> Pid ! cancel.
    
    my_timer(Time, Fun) ->
      receive 
        cancel ->
          void
      after Time ->
        Fun()
      end.

    다음과 같이 타이머를 만들 수 있다.

    1. 메세지는 항상 Time, Fun 형식으로 보내주서 after 절에서만 수행되도록 한다. 이 때, after 절에 있는 Time만큼 recieve 절에서 기다린다.
    2. 2. Time이 지나면, after 절의 Fun()이 실행된다.
    3. 3. cancel 함수가 호출되어서 해당 Pid로 cancle 아톰이 전달되면, 타이머는 cancel 아톰을 메세지로 전달받고, 패턴 매칭이 되어 정지한다. 

     

    8.6 선택적 수신

    Send

    • Send는 메세지를 프로세스의 메일 박스로 보낸다.

    Receive

    • Receive는 프로세스의 메일 박스에서 메세지를 하나씩 제거 시도한다. 
    • 얼랭 프로세스는 각 프로세스마다 하나의 메일 박스를 가진다. 메일박스는 receive 키워드를 할 때만 한번씩 검사된다. 

    Receive는 다음과 같이 동작한다.

    1. Recieve 절로 진입 시, after에 Time 설정을 확인하고 설정된 경우 타이머가 시작된다. 
    2. 메일박스에서 첫번째 메세지를 가져와서 식에 있는 모든 패턴 매치를 시도한다. 패턴 매치가 성공하면 메세지를 메일 박스에서 제거한다.
    3. 메일박스의 첫번째 메세지가 어떤 패턴과도 매치되지 않으면, 첫 메세지는 메일박스에서 제거되어 save Que에 들어간다. 이어서 메일 박스의 두번째 메세지를 꺼내서 패턴 매치를 시도한다. 이 과정은 매치되는 메세지를 발견 / 메일박스의 모든 메세지를 조사할 때까지 반복된다. 
    4. 메일박스의 어떤 메세지도 매치되지 않으면 프로세스는 멈추고, reschedule된다. 즉, 새로운 메세지가 들어오는 것을 기다린다. 새 메세지가 도착하면 recieve에서 패턴 매치를 다시 시도한다. 그렇지만 이 때, save Que에 있는 메세지를 다시 시도하지는 않는다.
    5. 만약 어떤 메세지가 매치되고 나면, 저장 큐에 들어갔던 메시지들은 모두 프로세스에 도착했던 순서대로 다시 메일박스로 들어간다. 만약 타이머가 설정되었다면 초기화된다. 
    important() ->
      receive
        {Priority, Message} when Priority > 10 ->
          [Message | important()]
      after 0 ->
        normal()
      end.
    
    normal() ->
      receive
        {_, Message} ->
          [Message | normal()]
      after 0 ->
        []
      end.
      
    >>>>>>>>>>>>>>>>>>>>>>>>>  
    1> c(multiproc).
    {ok,multiproc}
    2> self() ! {15, high}, self() ! {7, low}, self() ! {1, low}, self() ! {17, high}.      
    {17,high}
    3> multiproc:important().
    [high,high,low,low]
    • 위 명령어로 메세지를 선택적으로 수신할 수도 있다.
    • 먼저 스스로에게 4개의 메세지를 보낸 다음에 recieve 명령어를 호출할 때 마다 하나씩 메세지를 꺼내 가는 식으로 한다.
    • 메세지가 매칭되는 경우에는 [Message | important()]에서 모든 메세지가 소모된다. 
    • 메세지가 매칭되지 않는 경우에는 after 0에 의해서 normal() 함수를 호출해서 끊임없이 남은 Message를 소비해서 리스트를 반환한다.
    • 이 때, after 0를 주는 이유는 after 0를 주지 않으면 해당 프로세스가 패턴 매칭되는 메세지가 들어올 때까지 무한히 기다리기 때문이다.

     

     

     

    8.7 등록된 프로세스

    프로세스는 기본적으로 PID를 이용해서 통신한다. 따라서 통신을 하기 위해서 PID를 알 필요가 있는데, 이런 PID를 특정 이름으로 등록해서 좀 더 손쉽게 사용할 수 있는 방법이 존재한다. 이 방법은 4개의 BIF를 이용해서 사용할 수 있다. 

    register(AnAtom, Pid)

    Pid를 AnAtom 이름으로 등록한다. 이미 사용하고 있는 AnAtom이라면 등록 실패한다.

    unregister(AnAtom)

    AnAtom과 연관된 모든 등록을 제거한다. 

    만약 등록된 프로세스가 죽으면 자동으로 등록이 제거됨. 

    whereis(AnAtom)  -> Pid | undefined

    AnAtom이 등록되었는지 조사한다. 

    registered() -> [AnAtom::atom()]

    시스템에 있는 모든 등록된 프로세스들의 목록을 반환한다.

    예시

    15> Pid100 = spawn(fun() -> a1:loop() end).
    <0.127.0>
    
    16> register(myfunc, Pid100).
    true
    
    17> whereis(myfunc).
    <0.127.0>
    
    18> registered().
    [socket_registry,kernel_sup,inet_db,user,logger,
     logger_handler_watcher,standard_error_sup,
     global_name_server,file_server_2,user_drv,code_server,
     kernel_safe_sup,standard_error,myfunc,erts_code_purger,
     global_group,init,erl_prim_loader,application_controller,
     kernel_refc,erl_signal_server,logger_sup,rex,logger_proxy,
     logger_std_h_default]
    • 다음과 같이 사용할 수 있다.
    • 이 때, atom과 비슷한 용법으로 사용하지만 myfunc라고 하면 그냥 atom이 반환된다. 만약 등록한 PID에서 PID 값을 반환 받기 위해서는 whereis()를 이용해서 얻을 수 있다.

     

    8.8 병행 프로그램을 작성하는 방법

    start() ->
      spawn(fun() -> loop([]) end).
    
    
    rpc(Pid, Request) -> 
      Pid ! Request,
      receive
        {Pid, Response} -> Response
      end.
    
    loop(X) ->
      receive
        Any ->
          io:format("Received: ~p~n", [Any]),
          loop(X)
      end.

    기본적으로 다음 형태를 이용해서 병행 프로그램을 확장해나간다.

     

     

    8.9 꼬리재귀

    loop() ->
      receive
        {From, {rectangle, W, H}} ->
          From ! {self(), W * H},
          loop();
        {From, {circle, R}} ->
          From ! {self(), R * R * 3.14},
          loop()
      end.

    꼬리 재귀는 메세지를 받을 때 마다 메세지를 처리하고 이어서 즉시 loop()를 호출하는 형태의 재귀다. 가장 끝 부분(Tail)에 위치하면서 재귀를 하도록 하는 녀석이라는 거다. 보통 재귀를 하게 되면 함수가 호출되면서 Stack 영역에 새로운 값이 쌓인다. 그런데 무한 loop로 재귀를 하게 되는 경우, Stack 영역이 꽉 차게 되면서 OverFlow가 발생할 것이다. 그렇지만 위와 같이 구성된 꼬리재귀는 그 문제에서 자유롭다. 

    가장 마지막 부분에 loop()가 존재하게 되는 경우, 컴파일러가 단순히 첫 부분으로 jump()를 하게 만들면서 컴파일 할 수 있게 된다. 따라서 스택 공간을 소비하지 않고 무한 루프를 돌 수 있다는 것이다. 

    loop() ->
      receive
        {From, {rectangle, W, H}} ->
          From ! {self(), W * H},
          loop(),
          %% 루프 호출 뒤에 또 다른 함수 호출
          someOtherFunction();
        {From, {circle, R}} ->
          From ! {self(), R * R * 3.14},
          loop()
      end.

    루프 뒤에 또 다른 함수를 호출하는 형식이 있을 수 있다. 이 때 컴파일러는 loop()를 호출한 다음 다시 loop()를 호출한 지점으로 돌아와 someOtherFunction()을 호출해야한다고 인식한다. 따라서 스택공간에 someOtherFunc()의 주소를 스택에 저장하고 loop()의 첫번째 시작으로 점프한다. 즉, 계속 스택 공간을 낭비하게 된다. 

     

    8.10 MFA로 띄우기

    %% 기존 --> 정적임.
    spawn(Func)
    
    $$ MFA로 호출
    spawn(Mod, FuncName, Args)

    코드를 동적으로 업데이터 할 계획이 있는 경우라면 프로세스를 띄울 때 MFA(Module, Function, Args)를 이용해서 spawn을 띄우면 된다. 이것을 명시해서 함수를 띄우게 되면 실행 중인 프로세스가 사용되는 동안 새 버전의 모듈 코드가 컴파일되면, 그 새 버전의 모듈 코드로 프로세스가 정확하게 업데이트 되는 것을 보장한다

     

     

    번외1 (https://learnyousomeerlang.com/the-hitchhikers-guide-to-concurrency#dont-panic)

    -module(dolphins).
    -author("user").
    
    %% API
    -export([dolphin1/0]).
    
    
    dolphin1() ->
      receive
        do_a_flip ->
          io:format("How about no?~n");
        fish ->
          io:format("So long and thanks for all the fish!~n");
        _ ->
          io:format("Heh, we're smarter than you humans.~n")
      end.
    • dolphins 모듈에 dolphin1이라는 함수를 생성할 수 있다. 이 함수는 단 한번의 메세지만 받고, 그 메세지의 값을 패턴 매칭해서 각각 값을 출력하는 형태의 함수다. 
    Dolphin = spawn(dolphins, dolphin1, []).
    Dolphin ! "oh, hello dolphin!".
    
    
    >>>>
    Heh, we're smarter than you humans.
    "oh, hello dolphin!"
    
    
    Dolphin ! "oh, hello dolphin!".
    >>>>
    "oh, hello dolphin!"
    • MFA를 전달해서 프로세스를 생성하고, 이 프로세스에게 메세지를 보냈다. 이 메세지는 패턴 매칭 되는 부분이 "_" 이기 때문에 Heh, We're smarter than you humans라는 말만 나온다. 
    • 그리고 다시 한번 이 프로세스에 메세지를 보내면, 이 프로세스는 recieve를 단 한번만 듣고 그것을 정상 수행했다면 종료하기 때문에 더 이상 일을 하지 않는다. 따라서 원하던 응답은 오지 않는다.
    dolphin2() ->
      receive
        {From, do_a_flip} ->
          From ! "How about no?~n";
        {From, fish} ->
          From ! "So long and thanks for all the fish!~n";
        _ ->
          io:format("Heh, we're smarter than you humans.~n")
      end.
      
      
    >>>>>>>>>>>
    Pid = spawn(dolphins, dolphin2, []).
    Pid ! {self(), do_a_flip}.
    flush().
    
    >>>>>>>>
    Shell got a "How about no?"
    ok
    • 앞서 이야기 한 것처럼 프로세스가 메세지를 보냈을 때, 메세지를 정상적으로 받았는지를 확인하기 위해서는 반드시 "응답"이 필요하다. 이전에는 io:format()을 이용해서 호출하는 형태였는데 이 부분은 엄밀히 말하면 응답은 아니다. 따라서 실제로 응답하기 위한 코드를 작성하면 위와 같다. 
    • 메일을 보낼 때 자신의 PID를 알려주고, 받은 프로세스는 이 PID에게 메세지를 전달해주는 방식이다. 이를 이용하면 쉘 프로세스는 메세지를 받고 이 메세지를 flush().로 확인할 수 있다. 
    dolphin3() ->
      receive
        {From, do_a_flip} ->
          From ! "How about no?~n",
          dolphin3();
        {From, fish} ->
          From ! "So long and thanks for all the fish!~n",
          dolphin3();
        _ ->
          io:format("Heh, we're smarter than you humans.~n"),
          dolphin3()
      end.
    
    
    
    >>>>
    Pid = spawn(dolphins, dolphin3, []).
    Pid ! {self(), do_a_flip}.
    Pid ! {self(), do_a_flip}.
    Pid ! {self(), do_a_flip}.
    Pid ! {self(), do_a_flip}.
    Pid ! {self(), do_a_flip}.
    flush()
    
    >>>>>
    Shell got "How about no?"
    Shell got "How about no?"
    Shell got "How about no?"
    Shell got "How about no?"
    Shell got "How about no?"
    ok

    그런데 위 함수의 경우 한번 메세지를 받으면 더 이상 함수가 하는 일이 없기 때문에 종료한다. 그런데 프로세스는 메세지를 계속 받아야 하는 경우가 있을텐데 이 때는 꼬리재귀를 이용해서 메세지를 계속 받도록 한다. 

     

    번외2 -1 (https://learnyousomeerlang.com/more-on-multiprocessing

    mystore1() ->
      receive
        {From, {store, Food}} ->
          From ! {self(), ok},
          mystore();
        {From, {take, Food}} ->
          From ! {self(), not_found},
          mystore();
        terminate ->
          ok
      end.
      
      
    spawn(mystore, mystore1, []).
    • 위 명령어를 이용해서 냉장고 프로세스를 하나 만든다고 생각해보자. 그런데 위 프로세스의 문제점은 냉장고에 음식을 저장해도 그 값을 기억하는 곳이 없다는 것이다.
    • 위의 프로세스는 재귀를 이용하기 때문에 필요한 경우 재귀에 인자를 넘겨주면서 상태를 기억하도록 개선할 수 있다. 
    mystore2(FoodList) ->
      receive
        {From, {store, Food}} ->
          From ! {self(), ok},
          mystore2([Food|FoodList]);
    
        {From, {take, Food}} ->
          case lists:member(Food, FoodList) of
            true ->
              From ! {self(), {ok, Food}},
              mystore2(lists:delete(Food, FoodList));
            false ->
              from ! {self(), not_found},
              mystore2(FoodList)
          end;
    
        terminate ->
          ok
      end.
    
    
    >>>>
    
    Pid = spawn(mystore, mystore2, [[]]).
    Pid ! {self(), {store, "apple"}}.
    Pid ! {self(), {store, "banana"}}.
    Pid ! {self(), {store, "subak"}}.
    Pid ! {self(), {take, "subak"}}.
    Pid ! {self(), {take, "apple"}}.
    Pid ! {self(), {take, "banana"}}.
    flush().
    
    >>>>>
    Shell got {<0.82.0>,{ok,"apple"}}
    Shell got {<0.82.0>,{ok,"apple"}}
    Shell got {<0.82.0>,{ok,"apple"}}
    • 다음과 같이 빈 리스트를 넘겨줘서 이 문제를 해결할 수 있다. 빈 리스트에 store를 할 경우, 값을 저장한 다음에 재귀로 넘겨주는 방식으로 처리할 수 있다. 
    • 이 때 꼬리재귀를 이용해야 무한 루프를 돌았을 때 Stack Over Flow가 발생하지 않는다.

     

    번외 2-2(https://learnyousomeerlang.com/more-on-multiprocessing)

    store(Pid, Food) ->
      Pid ! {self(), {store, Food}},
      receive
        {Pid, Msg} -> Msg
      end.
    
    take(Pid, Food) ->
      Pid ! {self(), {take, Food}},
      receive
        {Pid, Msg} -> Msg
      end.
      
    start(FoodList) ->
      spawn(?MODULE, mystore2, [FoodList]).
    • 번외 2-1에서 추가적으로 코드를 개선하기 위해서 다음과 같이 접근했다. 
    • 기존에는 이 프로세스를 사용하기 위해서 사용자가 프로토콜을 모두 알아야 했다. 예를 들어 요청을 보낼 때 {store, Food}라고 보내야하는 것을 알아야 한다.
      • 이 부분을 추상화 하기 위해서 store / take 같은 함수를 이용해서 추상화 했다.
    • 프로세스를 생성하는 것도 귀찮을 수 있다. 따라서 start()에 초기 조건만 넘겨주면 프로세스를 생성하는 함수를 만든다.
    • 이 때 ?MODULE은 현재 모듈 이름을 자동으로 알려주는 매크로다.
    # 앞의 부분
    Pid = spawn(mystore, mystore2, [[]]).
    Pid ! {self(), {store, "apple"}}.
    Pid ! {self(), {store, "apple"}}.
    Pid ! {self(), {store, "apple"}}.
    
    # 개선 부분
    Pid = kitchen:start([dog, cat, banana]).
    kitchen:store(Pid, dog).
    kitchen:store(Pid, dog).
    kitchen:take(Pid, dog).

    위와 같은 내용을 적용하면 코드가 한편 더 깔끔해진다. 

     

     

     

     

     

     

    '프로그래밍 언어 > erlang' 카테고리의 다른 글

    erlang : 오류 및 프로세스  (0) 2022.10.12
    erlang : 재귀  (0) 2022.09.24
    erlang 9장 : 병행 프로그램과 오류  (1) 2022.09.21
    erlang 3장  (0) 2022.09.17
    erlang 2장  (0) 2022.09.06

    댓글

    Designed by JB FACTORY