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 이병준

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