gimmesilver's blog

Agbird.egloos.com

포토로그



하스켈 네트워크 프로그래밍...3 하스켈 스프링노트

쓰레드를 이용한 다중 접속 서버 만들기

하스켈 네트워크 프로그래밍...2 에서 예고한대로 이번에는 쓰레드를 이용한 다중 접속 서버를 만들어 보겠습니다.
쓰레드를 생성하는 방법은 매우 간단한데 Control.Concurrent 모듈내에 정의된 forkIO 라는 함수를 사용하면 됩니다. forkIO 함수는 스레드 작업을 수행할 IO 타입 함수를 파라미터로 받고 생성된 ThreadId 를 반환합니다. 아래는 간단한 쓰레드 예제입니다.

import Control.Concurrent

main = do
    forkIO printLoop 'a'    -- 1)
    printLoop 'b'             -- 2)

printLoop c = putChar c >> printLoop c -- C로 표현하자면 printLoop(char c) { for(;;) putchar(c); } 와 같음

forkIO 함수에 의해 1)에 있는 printLoop 함수는 별도의 쓰레드로 실행됩니다. 따라서 1)과 2)의 printLoop 함수는 병렬적으로 수행되어 'a'와 'b' 가 화면에 번갈아가며 무한 출력됩니다.

forkIO 를 이용해서 생성된 쓰레드는 OS 수준에서 생성되는 쓰레드보다 경량(lightweight)의 쓰레드입니다. 이것은 하스켈 프로그램을 실행시키는 런타임 시스템이 자체적으로 생성한 쓰레드이며 따라서 일반 쓰레드보다 생성이나 전환에 필요한 오버헤드가 적습니다. 즉, OS 수준에서 볼 때는 main은 하나의 쓰레드이며 이 main 쓰레드 자체에서 두 함수를 적절하게 스위칭해주는 것입니다.
만약 OS 수준의 쓰레드를 생성하려면 forkIO 대신에 forkOS 라는 함수를 사용합니다. forkOS는 내부적으로 pthread_create() 나 CreateThread()와 같은 시스템 API 를 호출하여 쓰레드를 생성합니다.
따라서 forkIO와 forkOS가 생성한 쓰레드는 몇 가지 차이점이 있는데 결정적으로 쓰레드 지역 공간(TLS:Thread Local Strorage)를 갖느냐 그렇지 않느냐의 차이가 있습니다.(TLS에 대한 보다 자세한 설명은 위키피이아의 TLS설명부분이나 디버그랩의 윈도우즈 TLS 설명 글을 참고하세요.) 때문에 이런 TLS 특성을 이용하는 외부 라이브러리를 사용한다면 forkIO가 아닌 forkOS를 사용해야 합니다. 물론 그 외의 경우에는 대부분 forkIO를 사용하는 것이 더 효율적입니다.
그 외에 쓰레드에 대한 자세한 설명은 다음에 기회가 되면 하도록 하고 다중 접속 서버를 만들어 보도록 하겠습니다.

import Network
import IO
import Control.Concurrent

MultiServer = withSocketsDo MultiServer'
MultiServer' = listenOn (PortNumber 10000) >>= (connLoop 1)    -- 1)

connLoop n sock = do
    (h,host,port) <- accept sock
    let clientName = (show n) ++ "th client"
    putStrLn (clientName ++ " is connected")
    forkIO $ catch (readLoop h clientName) print        -- 2)
    connLoop (n+1) sock


readLoop h name = do
    contents <- hGetLine h
    putStrLn (name ++ " says: " ++ contents)
    if (contents /= "bye")
        then readLoop h name
        else return ()

1) 은 이전 글에서 언급했듯이 모나드를 이용한 구문입니다. connLoop 함수는 각각의 접속자를 구분하기 위한 id 값과 - listenOn 함수가 반환하는 - 소켓 핸들을 파라미터로 받습니다. 여기서 소켓 핸들을 모나드를 통해 전달하는 것입니다.
여기서 하스켈의 특징이 하나 나타납니다. connLoop 함수는 파라미터를 두 개 받는 함수인데 모나드는 하나만 전달이 가능하므로 원래는 모나드를 통해 값을 전달하는 것이 불가능합니다. 하지만 connLoop가 첫번째 파라미터를 받은 상태인 (connLoop 1) 이라는 형태로 변형되면 소켓 핸들 하나만 받을 수 있는 함수로 바뀌므로 모나드를 적용할 수 있습니다.(이 부분이 잘 이해가 가지 않으면 make it functional의 하스켈 함수 설명 부분을 참고하세요.)
첨언하자면, 하스켈의 파라미터 처리 방식은 C,C++,Java 같은 명령형 프로그래밍 언어와 다릅니다. 명령형 언어는 파라미터 여러 개의 하나의 묶음으로 받아 동시에 처리하지만 하스켈은 파라미터를 왼쪽부터 순서대로 하나씩 받아 새로운 함수를 반환하는 형태로 동작합니다.
예를 들어 명령형 언어에서는 foo(x, y, z) 라는 함수가 있으면 foo 함수 호출 시 (x,y,z) 를 한꺼번에 foo 함수에 넘겨서 처리하지만, 하스켈에서 foo x y z 라고 하면 아래와 같이 처리됩니다.

(((foo x) y) z)

이런 특성을 갖는 함수를 '커리(curry) 함수'라고 합니다. 하스켈은 기본적으로 커리 함수로 동작합니다. 기회가 되면 계속 예를 들겠지만 이런 커리 함수 특성은 - 역시 나중에 소개가 되겠지만 - '고차함수', 함수 합성 등과 함께 재사용을 극대화해주는 하스켈의 특징입니다.

2) readLoop 함수를 별도의 쓰레드에서 처리하도록 해줍니다. 바로 다음 줄에서 connLoop 를 재귀적으로 호출하기 때문에 어떤 클라이언트가 접속하면 해당 클라이언트의 데이터를 처리하는 readLoop 함수가 쓰레드로 실행되면서 입력값을 처리하는 동시에 다른 클라이언트의 접속을 처리할 수 있습니다.

다음 번에는 쓰레드 동기화 기능을 포함한 간단한 채팅 서버를 만들어 보겠습니다.


덧글

댓글 입력 영역