왜 그냥 fork
를 쓰지 않고, async
가 필요한지 알아 보겠습니다.
forkIO :: IO () -> IO ThreadId
forkIO
는 받는 작업이 IO a
가 아니라, IO ()
입니다. 결과값이 없는 IO
작업만 받습니다.
하스켈에선 한 쓰레드에서 다른 쓰레드의 결과값에 바로 접근할 수 없습니다. 그러니 당연히 결과값이 없는 IO ()
작업만 받으면 됩니다.
그럼, 결과값이 필요한 IO a 작업은 어떻게 fork 시킬까요?
기본 아이디어는 IO a
의 결과값 a
타입 값을 받아 올 MVar
변수를 만들어, IO
작업의 결과를 거기에 넣는 작업을 만드는 것입니다.
import Control.Concurrent
import Control.Concurrent.STM
-- IO () 타입이 아니기 때문에 바로 fork 할 수 없습니다.
oneChar :: IO Char
= do
oneChar <- getChar
c return c
-- oneChar 를 fork해서 실행한다면 결과값을 어떻게 받아 올까요?
-- 애초에 fork 는 IO () 타입 작업만 받으니, 이대로는 fork할 수 없습니다.
-- 아래와 같이 원래 작업을 부르는 IO () 타입의 작업을 만듭니다.
-- 이 작업은 MVar 성격의 변수를 받아 작업 결과를 그 MVar에 넣기만 하니, 결과 타입은 IO () 입니다.
oneCharFork :: TMVar Char -> IO ()
= do
oneCharFork resultVar <- oneChar
result $ putTMVar resultVar result
atomically
main :: IO ()
= do
main <- newEmptyTMVarIO
v <- forkIO (oneCharFork v) -- fork할 때, 외부에서 만든 MVar를 같이 넘깁니다.
tid <- atomically (readTMVar v)
r putChar r
위와 같이 작업하면 다른 쓰레드의 결과값을 받아 올 수 있습니다. 소통 채널로 MVar
를 쓰는 형태로 만들 수 밖에 없습니다.
async
함수도 같은 아이디어로 되어 있고, 예외 핸들링을 위해 mask
로 짜여져 있습니다. 그리고, 나중에 결과값에 접근 할 수 있도록 MVar
변수와 쓰레드 ID를 묶은 Async
값을 만들어 리턴합니다. 이제 나중에 Async
안에 들어있는 MVar
값에 Read
를 걸어두면, 결과값을 가져올 수 있고, 쓰레드가 종료하는 걸 알아낼 수도 있습니다.
async :: IO a -> IO (Async a)
= do
async action <- newEmptyVar
m <- mask $ \restore -> forkIO
t do -- 원래 IO a 였던 액션을 IO () 으로 바꾸고, 타입 a 값은 MVar에 담아 놓습니다.
(<- try (restore action)
r
putMVar m r
)return (Async t m)
Async
작업 체인을 만드는 withAsync
의 구현을 보면,
withAsync :: IO a -> (Async a -> IO b) -> IO b
= inline withAsyncUsing rawForkIO
withAsync -- bracket을 써서 구현해도 되지만 느려서 hand-coding 했다고 합니다.
withAsyncUsing :: (IO () -> IO ThreadId) -> IO a -> (Async a -> IO b) -> IO b
= \action inner -> do
withAsyncUsing doFork <- newEmptyTMVarIO
var $ \restore -> do
mask <- doFork $ try (restore action) >>= atomically . putTMVar var
t -- 이 부분이 async 구현이랑 같습니다.
let a = Async t (readTMVar var)
<- restore (inner a) `catchAll` \e -> do
r
uninterruptibleCancel a
throwIO e
uninterruptibleCancel areturn r
fpcomplete async 문서
withAsync 실제 소스