gimmesilver's blog

Agbird.egloos.com

포토로그



하스켈 멀티쓰레드 동기화 프로그래밍 하스켈 스프링노트

하스켈 동기화 객체 MVar

멀티쓰레드 관련 기능은 Control.Concurrent 모듈에 있습니다. 그리고 이 모듈에서는 여러 쓰레드간에 동기를 위해 몇 가지 함수와 데이터 타입을 제공하는데 그 중 하나가 지금부터 살펴볼 MVar 입니다. 우선 MVar에 관련된 기본 함수 3가지를 알아보겠습니다.

newMVar :: a -> IO (MVar a) : MVar 객체를 생성하고 그 객체를 반환합니다. 이 때 파라미터로 MVar 가 저장할 데이터의 초기값을 지정합니다. 예를 들어 MVar 객체를 생성하면서 이 객체에 3 이라는 값을 넣으려면 아래와 같이 작성합니다.

  1. mvar <- newMVar 3

물론 리스트나 문자열도 가능합니다.

  1. mvar <- newMVar [1,2,3] 또는 mvar <- newMVar "Hello,World"


만약 MVar를 단순히 세마포어나 뮤텍스 역할만을 수행하도록 (다시 말하면 특별히 데이터를 저장하고 싶지 않으면) 하려면 파라미터로 () 를 전달합니다. (객체 지향 프로그래밍에서의 null object 와 비슷합니다.)

  1. mvar <- newMVar ()


takeMVar :: MVar a -> IO a : MVar 객체가 저장하고 있는 데이터를 꺼냅니다.
putMVar :: MVar a -> a -> IO () : MVar 객체안에 데이터를 저장합니다.


takeMVar 나 putMVar 역시 단순히 동기화 기능만 수행하고자 한다면 () 데이터를 넣었다 빼도록 작성하면 됩니다.

MVar 는 어떤 값을 저장할 수 있는 상자와 같습니다. 이 상자에는 데이터를 넣었다 뺐다 할 수 있는데 넣고 빼는 동작은 원자적(atomic)입니다. 즉, 한번에 한 쓰레드만 상자에 접근이 가능합니다. 따라서 MVar는 여러 개의 쓰레드가 접근하더라도 항상 어느 한 순간에는 '꽉찬(full)' 상태 혹은 '텅빈(empty)' 상태입니다. 
만약 상자에 데이터가 들어 있는 상태(full)에서 데이터를 넣으려는(putMVar 함수 호출) 쓰레드가 있으면 그 쓰레드는 그 상자가 비워질 때(empty)까지 대기합니다. 반대로 상자에서 데이터를 꺼내려는(takeMVar 함수 호출) 쓰레드는 그 상자에 데이터가 찰 때까지 대기합니다.
뭐 여기까지는 기존에 우리가 사용하던 뮤텍스나 세마포어와 크게 다르지 않습니다. 약간의 차이가 있다면 대개의 경우 뮤텍스나 세마포어는 해당 객체를 획득할 때만 블럭 상태에 빠지게 되고 획득한 뮤텍스(혹은 세마포어)를 해제할 때는 블럭 상태에 빠지지 않습니다. 즉,

  1. getMutext();
    releaseMutex();
    releaseMutex();

이렇게 하면 두번째 releaseMutext() 에서는 이미 해제한 뮤텍스를 또 해제하려고 하므로 그냥 에러가 발생합니다. 그러나 하스켈의 MVar는,

  1. mvar = newMVar ()
    takeMVar mvar
    putMVar mvar ()
    putMVar mvar ()

 이렇게하면 두번째 putMVar 수행 시 이미 MVar 객체는 데이터가 들어가 있는 상태이므로 다른 쓰레드가 MVar에서 데이터를 꺼낼 때 까지 대기합니다.


 아래는 이런 MVar 특성을 이용한 간단한 예제입니다. 먼저 MVar를 사용하지 않았을 때의 소스는 아래와 같습니다.

  1. raceTest = do
        forkIO $ putStrLn "Hello," -- other thread
        putStrLn "World!" -- main thread

이 함수를 실행하면 "Hello,"라는 문자열과 "World!"라는 문자열이 뒤섞여서 출력됩니다. 이제 "Hello,"와 "World!"가 차례로 출력되도록 MVar를 이용해서 동기화 시키면 아래와 같습니다.

  1. mvarTest = do
        mvar = newMVar () -- 1)
        forkIO $ otherThread mvar -- 2)
        putMVar mvar () -- 3)
        putStrLn "World!"

    otherThread mvar = do
        putStrLn "Hello,"
       takeMVar mvar -- 4)


1) MVar 객체 하나를 생성합니다. 위에서 언급했듯이 특별히 저장할 데이터가 없으므로 newMVar () 라고 합니다. 주의할 점은 newMVar () 라고 호출하는 것이 생성된 MVar 객체가 '텅빈' 상태인 것을 의미하는 것은 아니라는 점입니다. () 를 MVar 에 넣고 빼는 동작은 실제 어떤 데이터를 저장하는 것이 아니지만 MVar 의 상태는 바꿉니다.

2) 쓰레드를 하나 생성하면서 동기화 객체를 파라미터로 넘겨줍니다.

3) 메인 쓰레드는 생성한 mvar 객체에 데이터를 넣으려고 하지만 이미 생성 당시 mvar 객체는 꽉 찬 상태이므로 다른 쓰레드가 takeMVar 를 호출할 때까지 대기합니다.

4) 메인이 생성한 다른 쓰레드는 "Hello," 라는 문자열을 화면에 출력하고 mvar 객체안에 있는 데이터를 꺼냅니다. 이제 메인 쓰레드는 mvar 객체에 데이터가 없으므로 putMVar 가 수행되고 "World!" 라는 문자열을 출력할 수 있습니다. 따라서 위 두 쓰레드는 경쟁 상태없이 "Hello,'와 'World!'라는 문자열을 차례대로 출력합니다.

참고로 위 동기화 예제는 모나드 메소드인 >>= 와 >> 를 사용하면 아래와 같이 짧게 줄여서 표현할 수 있습니다.


  1. mvarTest2 = do
        m <- newMVar ()
        forkIO (putStrLn "Hello," >> takeMVar m)
        putMVar m () >> putStrLn "World!"


혹은 람다 함수를 이용할 수도 있습니다.


  1. mvarTest3 = newMVar () >>= (\m ->
        forkIO (putStrLn "Hello," >> takeMVar m) >>
        putMVar m () >> putStrLn "World!" )


뭐 그다지 중요한 사항은 아니지만 조금씩 이런 표현에 익숙해지시길 바라는 의미에서 언급해봤습니다.
MVar는 takeMVar와 putMVar 외에도 몇 가지 추가적인 함수를 제공합니다. 그 중 몇 가지를 살펴 보면

tryTakeMVar / tryPutMVar : 호출 쓰레드가 블럭되지 않는다는 것만 제외하면 각각 takeMVar / putMVar 와 같습니다.

readMVar :: MVar a -> IO a : 해당 MVar 객체안에 있는 데이터를 꺼내서 그 값의 복사본을 반환하고 원래 데이터는 다시 MVar 에 저장합니다. 즉, 이 함수는 아래와 같이 동작합니다.

  1. readMVar mvar = do
        mvarData <- takeMVar mvar
        putMVar mvar mvarData
        return mvarData


swapMVar :: MVar a -> a -> IO a : 이름 그대로 MVar 객체안에 있는 데이터를 파라미터로 넘겨준 데이터로 교체합니다.
예)

  1. mvar = newMVar "Hello,"
    s <- swapMVar mvar "World!"     -- mvar 객체에는 "Hello," 대신 "World!"가 저장
    putStrLn s             -- "Hello," 출력


modifyMVar_ :: MVar a -> (a -> IO a) -> IO (): MVar 객체 값을 수정합니다. 파라미터로 데이터 수정을 할 함수를 넘겨주는데 만약 이 함수에서 예외가 발생하더라도 modifyMVar_ 는 MVar 객체에 원래 데이터를 안전하게 저장하고 예외를 넘깁니다.
예)

  1. mvar <- newMVar "Hello,"
    modifyMVar_ mvar (return . (++"World!"))
    takeMVar mvar >>= putStrLn    
  2. -- 화면에 "Hello,World!" 가 출력됨

덧글

댓글 입력 영역