Languages/Erlang2008.09.22 17:19

앞선 글을 통해 Erlang에서 병렬 프로그래밍이 어떻게 이루어지는지 대략이나마 감을 잡으실 수 있었을 거라고 믿습니다. 다시 정리해보자면, 얼랭에서 병렬 프로그램은 프로세스의 집합으로 만들어지고, 프로세스 끼리 메시지를 주고받아 통신하는 방식을 사용해 구현됩니다. 메시지를 보낼때는 Pid ! Message 이런 형식으로 보내고, 받는 쪽에서는 receive 를 사용해 메시지를 받아 처리합니다.

그러면 지금부터는 앞선 글에서 만든 프로그램을 진짜 클라이언트/서버 모델을 만족하도록 바꿔 보겠습니다. 클라이언트가 서버에게 요청을 보내면, 서버는 그 요청을 해석해서 클라이언트에게 답을 보내주어야 합니다. 다음의 코드를 보시죠.

-module(concur).
-export([start_server/0, send_request/2, send_stop/1]).

start_server() -> spawn( fun loop/0 ).

loop() ->
    receive
        {request, Pid, L} ->
            Result = odds_and_evens_acc(L),
            Pid ! {response, self(), Result},
            loop();
        stop ->
            exit
    end.

send_request(Pid, L) ->
    Pid ! {request, self(), L},
    receive
        {response, _SPid, {{odd, Ov},{even,Ev}}} ->
            io:format("odd: ~p, even: ~p~n", [Ov,Ev])
    end.

send_stop(Pid) -> Pid ! stop.

odds_and_evens_acc(L) ->
    odds_and_evens_acc(L, 0, 0).

odds_and_evens_acc([H|T], Odds, Evens) ->
    case ( H rem 2 ) of
        1 -> odds_and_evens_acc(T, Odds + H, Evens);
        0 -> odds_and_evens_acc(T, Odds, Evens + H)
    end;
odds_and_evens_acc([], Odds, Evens) ->
    { {odd, Odds}, {even, Evens} }.

클라이언트/서버 모델을 따르는 통신을 하기 위해, 서버에게 보내는 메시지의 형식이 좀 바뀌었습니다. 메시지가 투플 형태로 간다는 것은 종전과 똑같습니다만, 그 투플의 두 번째 항목에 요청을 보내는 클라이언트 프로세스의 PID가 들어간다는 점이 달라졌습니다. 이 PID가 있어야 서버는 어느 클라이언트에게 응답을 보내야 할 지를 알 수  있죠. 서버는 요청이 오면 odds_and_evens_acc를 호출해서 결과를 계산한 다음에, 요청을 보낸 프로세스에게 응답을 전송합니다. 전송되는 응답 투플 맨 앞에는 '응답'임을 알리기 위해 response라는 아톰이 들어가고, 투플 두번째 항목에는 서버 프로세스의 PID가 들어가고(self()를 호출하면 자기 자신의 PID를 얻을 수 있습니다) 세 번째 항목에는 보낼 계산 결과값이 들어갑니다. 

클라이언트는 서버에게 요청을 보내기 위해 send_request 함수를 이용해야 합니다. 이 함수의 첫번째 인자로는 서버의 PID가 오고, 두 번째 인자로는 리스트가 전달됩니다. send_request는 해당 인자들을 사용해 서버에 메시지를 전송한 다음, 스스로 receive를 통해 그 서버가 보내는 응답을 전송받습니다. 그런 다음 응답을 해석해 화면에 결과값을 뿌리죠. (서버가 보내는 PID는 사용하지 않을 것이기 때문에 _SPid와 같은 이름의 변수를 썼다는 것에 주의합시다. _가 앞에 붙는 변수는 설사 이후의 코드에서 사용되지 않아도 컴파일러가 ok합니다.)

위의 코드는 다음과 같이 사용합니다.

14> c(concur).
{ok,concur}
15> Pid = concur:start_server().
<0.90.0>
16> concur:send_request(Pid, [1,2,3,4,5,6]).
odd: 9, even: 12
ok
17>

이렇게 해서 클라이언트/서버 모델을 따르는 간단한 예제까지 만들어 봤습니다. 이해하기 쉬우셨는지 모르겠군요. 다음 글에서는 실제로 메시지 송수신 과정에서 발생할 수 있는 문제에 대처할 수 있는 코드를 만드는 방법을 포함해, 조금 더 심화된 주제들을 살펴보겠습니다.

머리가 터질거같은 상태에서 억지로 글을 쓰다보니 더 이상 길게는 못쓰겠군요. 머리 터질때 코드 작성하는 일 말고는 할 수 있는게 없는 저 자신도 참 한심하기 짝이 없습니다만....




신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Erlang2008.09.22 13:46

앞선 다섯 번의 글을 통해, Erlang의 가장 기본적인 면들을 살펴 봤습니다. 물론 다 살펴본 것은 아닙니다만, 미처 살펴보지 못한 주제들(binary, record...)은 Erlang의 가장 기본적인 부분이라기 보다는 양념에 가깝고, 특별히 심각한 프로그래밍을 할 생각이 없다면 일단은 제처놓고 사용하지 않을 수도 있습니다. 그러니, 나중에 필요할 때 더 살펴보도록 하죠.

그러니, 오늘은 바로 병행성(concurrency)으로 점프해 보도록 하겠습니다. 사실 얼랭을 배우겠다고 작심하신 분들 중 상당수는 Erlang의 병렬 처리 성능에 이끌려 오신 분들일 거에요. CPU를 꽂으면 꽂는 대로 성능이 graceful 하게 upgrader되는 시스템은 모든 개발자가 꿈꾸는 시스템이긴 합니다만, 그렇다고 모든 사람들이 그런 프로그램을 짤 수 있는 것은 아니었죠. (언어의 가상 머신 시스템이 발전되어 가면서 그 장벽이 점점 낮아지고 있는 것은 사실입니다만... )

Erlang의 병행성은 spawn이라는 fork() call과 메시지 send / receive 메커니즘을 톻해 얻어집니다. 그 메커니즘이 동작하는 방식이 굉장히 간단하고 알기 쉽기 떄문에, C에서 fork()를 써 보신 분이나, 병행 프로그래밍에 대해 아주 간단한 수준의 지식만 가지고 있더라도 프로그래밍을 할 수가 있어요.

우선, 예전에 만들었던 함수들을 가지고, 그 함수들을 사용해 병행성 프로그램을 만드는 실습을 한번 해 보겠습니다. 다음 코드를 보시죠.

-module(concur).
-export([odds_and_evens_acc/1]).

odds_and_evens_acc(L) ->
    odds_and_evens_acc(L, 0, 0).

odds_and_evens_acc([H|T], Odds, Evens) ->
    case ( H rem 2 ) of
        1 -> odds_and_evens_acc(T, Odds + H, Evens);
        0 -> odds_and_evens_acc(T, Odds, Evens + H)
    end;
odds_and_evens_acc([], Odds, Evens) ->
    { {odd, Odds}, {even, Evens} }.

이 함수를 컴파일하고 실행하면 다음과 같은 결과를 얻습니다.

Eshell V5.5.5  (abort with ^G)
1> c(concur).
{ok,concur}
2> concur:odds_and_evens_acc([1,2,3,4,5,6,7,8,9,10]).
{{odd,25},{even,30}}
3>

뭐 여기까지는 단순한데요. 자. 그러면 이 함수를 별개의 프로세스로 실행 시킬 수 있도록, 코드를 변경해 보도록 하죠. 목표는 odds_and_evens_acc 함수가 서버로서 계속해서 동작할 수 있도록 만드는 것입니다. 클라이언트 측에서는 이 서버에 메시지를 전달하는 것만으로 리스트의 홀수합과 짝수합을 계산할 수 있어야 하죠.

우선 서버라는 것은 뭔가가 계속해서 끊임없이 돌아야 하는 것이니까, 그 끊임없이 도는 함수를 한 번 만들어 보겠습니다. 메시지를 수신할 때 쓰는 receive 구문이 포함되어 있으니까 주의해서 보시기 바랍니다.

-module(concur).
-export([start/0]).

start() -> spawn( fun loop/0 ).

loop() ->
    receive
        L ->
            {{odd, Ov}, {even, Ev}} = odds_and_evens_acc(L),
            io:format("~p, ~p~n", [Ov, Ev]),
            loop()
    end.

odds_and_evens_acc(L) ->
    odds_and_evens_acc(L, 0, 0).

odds_and_evens_acc([H|T], Odds, Evens) ->
    case ( H rem 2 ) of
        1 -> odds_and_evens_acc(T, Odds + H, Evens);
        0 -> odds_and_evens_acc(T, Odds, Evens + H)
    end;


odds_and_evens_acc([], Odds, Evens) ->
    { {odd, Odds}, {even, Evens} }.

이 코드는 다음과 같이 실행합니다.

23> c(concur).
{ok,concur}
24> Q = concur:start().
<0.95.0>
25> Q ! [1,2,3].
[1,2,3]
4, 2
27>

concur 모듈에 정의되어 있는 start() 함수를 부르면, spawn 함수가 호출되면서 새로운 프로세스가 만들어지고, 그 프로세스의 PID가 반환됩니다. spawn 함수의 인자로 fun loop/0을 넘겼기 때문에, 해당 함수가 그 프로세스의 이미지(image)가 됩니다. 이 함수는 메시지가 도착할 때 마다 (이 메시지는 리스트입니다) 그 메시지를 인자로 하여 odds_and_evens_acc 함수를 호출하고, 그 결과를 화면에 출력합니다. 서버는 계속해서 돌아야 하기 때문에, 실행이 끝나면 loop()로 자기 자신을 호출하여 다음 메시지를 기다립니다. 간단하죠? 클라이언트 측에서는 이 서버에 메시지를 전송하려면 서버의 PID에다가 !를 주고 메시지를 인자로 넘기기만 하면 됩니다. 그러면 해당 PID를 갖는 서버 프로세스에 메시지가 날아가죠.

그런데 이 함수에는 좀 미심쩍은 구석이 있습니다. 메시지로 받은 L이 리스트가 아니면 어떻게 되는거죠?

그런 미심쩍음을 좀 해소하기 위해, 이번에는 리스트와 함께 아톰으로 된 헤더를 붙여 전달해야 처리되도록 코드를 바꿔 보죠. loop()의 코드만 다음과 같이 바꿔보겠습니다.

loop() ->
    receive
        {list, L} ->
            {{odd, Ov}, {even, Ev}} = odds_and_evens_acc(L),
            io:format("~p, ~p~n", [Ov, Ev]),
            loop();
        stop ->
            exit
    end.

그렇게 한 다음에 다음과 같이 실행해 보죠.

27> f().
ok
28> c(concur).
{ok,concur}
29> Q = concur:start().
<0.104.0>
30> Q ! [1,2,3].
[1,2,3]
31> Q ! {list, [1,2,3]}.
{list,[1,2,3]}
4, 2
32> Q ! stop.
stop
33> Q ! {list, [1,2,3]}.
{list,[1,2,3]}
34>

이제 서버의 함수가 정상적으로 실행되도록 하려면, 메시지 앞에 list라는 아톰을 붙여야 합니다. 다른 형태의 메시지를 보내면 (list가 아닌 다른 아톰을 헤더로 사용하거나 아예 형식이 다른 메시지를 보내면) 그 메시지는 아예 loop 함수에 의해 처리가 되지 않는다는 것도 볼 수 있습니다. (위의 30>번 실행 결과를 보세요.) 서버의 동작에 영향을 미치지 못하는 거죠. (이 점이 Erlang 서버의 구현을 좀 더 편하게 만든다고도 볼 수 있습니다. 명시적으로 무시하는 로직을 작성할 수도 있습니다만, 그렇게 하지 않아도 어쨌든 되긴 된다는 거죠. )

서버의 동작을 중지시키려면 stop이라는 메시지를 보내면 되고, 그런 다음에는 concur:start()를 호출한 결과로 받은 PID에 대고 메시지를 전송시켜 봐야 아무일도 하지 않습니다. 서버의 loop() 함수 수행이 이미 끝났거든요. (32>와 32>을 보세요.) 어쨌거나, 위의 코드에는 아직도 L이 리스트인지 아닌지를 검사하는 부분은 없습니다. 다음과 같이 한번 바꿔보죠.

loop() ->
    receive
        {list, L} ->
            case is_list(L) of
                true ->
                    {{odd, Ov}, {even, Ev}} = odds_and_evens_acc(L),
                    io:format("~p, ~p~n", [Ov, Ev]),
                    loop();
                false ->
                    io:format("second argument is not a list"),
                    loop()
            end;
        stop ->
            exit
    end.

이렇게 한 다음에 다음과 같이 실행해 보겠습니다.

3> c(concur).
{ok,concur}
4> Pid = concur:start().
<0.46.0>
5> Pid ! { list, [1,2,3,4,5] }.
{list,[1,2,3,4,5]}
9, 6
6> Pid ! { list, 3 }.
{list,3}
second argument is not a list
7>

어떤 멍청한 인간이 리스트가 아닌 다른 무엇을 보내도 사고가 생기지 않는 코드를 만들고자 이렇게 했습니다. 종전 코드에서는, 클라이언트가 만일 리스트가 아닌 다른 무엇을 서버에 보냈더라면 실행 시간에 오류가 발생했을 겁니다. 이처럼, 얼랭으로 프로그램을 짤 때 문제가 될 수 있는 부분은, 대응규칙(matching rule)으로 처리될 수 없는 비정상적인 어떤 일이 벌어졌을 때가 대부분입니다. 그런 부분을 막도록 프로그램을 잘 짜야 괴상망칙한 일이 벌어지는 것을 미연에 방지할 수 있죠.

자. 그렇다면 이제 list라는 아톰 헤더는 필요가 없는거군요? 그렇습니다. list라는 아톰 대신, 뭔가 다른 그럴싸한 아톰이 오면 좋을 것 같습니다. 어쩄든 '이 메시지가 무슨 메시지인지를' 표현하는 헤더는 필요한 것이니까요. 다음 글에서 예제를 그에 맞게 조금 수정하도록 하겠습니다. 그리고 지금까지의 예제는 클라이언트가 메시지를 보내면 서버가 그 실행 결과를 화면에 뿌리는 것이었습니다만, 다음 글에서는 이제 진짜 클라이언트-서버 모델에 맞게 프로그램을 더 고쳐보겠습니다. 그러려면 고려해야 할 것들이 좀 더 많습니다.



신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Erlang2008.06.12 19:16

검토자로 참여했던 Programming Erlang의 한국어 번역판, "프로그래밍 얼랭"을 방금 받았습니다. 이 책을 받아들고 보니, 예전에 APNOMS 2007에서 만났던 Romain Lenglet 이라는 친구가 생각나네요. 이 친구는 그당시 Tokyo의 Orange Labs라는 곳에서 일하고 있었는데, French Telecom 직원이었고, 프로그래밍 언어로 Erlang을 쓴다고 했었습니다. 그런데 "얼랭"이라고 하니까 잘 못알아듣더군요. 그친구는 거의 "랭"과 "랑"의 중간쯤 되는 발음을 헀던 것 같습니다. (제가 술에 취해 있어놔서 잘 못들었을수도 있습니다. ㅋㅋ)

제가 그 친구로부터 얼랭에 관한 이야기를 듣고 관심을 가지기 시작한지 일년만에, 이 책의 번역에 조금이나마 기여를 할 수 있게 된걸 보면, 사람 사이의 인연은 그냥 만들어지는 것은 아닌 모양입니다.

사용자 삽입 이미지

지난번에 이집트 출장을 갔다 오는 길에는 비행기 안에서 피지에서 일하는 의사 한분을 만났습니다. 환갑을 지나 70을 향해 달려가고 있는 분이었는데, 짧은 영어로 여러가지 재미있는 이야기를 나누었었죠. 제가 가지고 있는 지병(?)들에 대한 이야기도 했습니다. 그분은 의사니까, 아무래도 제 병에 관심이 있었겠죠. 그것도 그 분이 가지고 있는 직업병이라면 직업병입니다만..

APNOMS에서 만난 그 친구와 Erlang에 대한 이야기를 나누게 된 것도, 아마 프로그래머가 공통적으로 가지고 있는 직업병이라면 직업병 탓이겠죠. 술먹으면서도 고작 프로그래밍 언어에 대해 이야기를 하고 있다니, 대체 우리는 어떻게 된 사람들인거야, 뭐 이런 소리를 하면서 웃었던 기억도 납니다. 어쨌든, 그런 병(?) 덕분에 저는 새로운 언어를 맛이나마 볼수 있었고, 새 책이 태어나는 데 조금이나마 기여를 할 수가 있었습니다.

 
사용자 삽입 이미지
Romain씨와 만났을 당시, 만취해있었던 접니다. -_-;


가끔은 그런 생각이 듭니다. 내게 주어지는 모든 것에는 다 이유가 있다는, 뭐 그런 생각 말이죠. 물론, 그런 생각을 너무 심각하게 하다 보면 인생이 좀 공포스러워지긴 합니다만.... 위의 만취샷이 주는 공포감에는 비할 수가 없을거 같기도 하군요. -_-

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 비밀댓글입니다

    2008.06.13 13:02 [ ADDR : EDIT/ DEL : REPLY ]
    • 무슨 그런 말씀을 -_-; 충분히 잘 하셨다고 생각합니다. :-)
      좋은 책 내느라 애쓰셨습니다. 열심히 보겠습니다~

      2008.06.13 13:36 신고 [ ADDR : EDIT/ DEL ]