Concurrency: Threads, MVar, and async
Prerequisites: IO and do-notation
GHC's threads are green threads: extremely cheap (around a kilobyte of memory each), scheduled by the Haskell runtime over a small number of operating system threads. Running 100,000 of them is normal. Immutable data means most of your code is safe to share between threads by default.
forkIO and MVar
import Control.Concurrent
import Control.Monad (forM, forM_)
worker :: Int -> MVar Int -> IO ()
worker n result = do
threadDelay (1000 * (5 - n))
putMVar result (n * n)
main :: IO ()
main = do
vars <- forM [1 .. 4] $ \n -> do
v <- newEmptyMVar
_ <- forkIO (worker n v)
return v
results <- mapM takeMVar vars
print results
counter <- newMVar (0 :: Int)
done <- newEmptyMVar
forM_ [1 .. 10] $ \_ -> forkIO $ do
modifyMVar_ counter (return . (+ 1))
putMVar done ()
forM_ [1 .. 10] $ \_ -> takeMVar done
readMVar counter >>= print
[1,4,9,16]
10
forkIOstarts a new thread running an IO action.- An MVar is a box that is either empty or full.
takeMVarwaits until it's full and empties it;putMVarwaits until it's empty and fills it. That makes it a lock, a result slot, and a one-place channel. modifyMVar_updates the contents safely — other threads wait.- The results come back in order
[1,4,9,16]because we wait on each MVar in turn, even though the threads finish in the opposite order.
The async library
Raw forkIO makes it easy to lose exceptions and leak threads. The
async package is the standard, safer interface:
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
fetch :: String -> IO String
fetch url = do
threadDelay 10000
return ("<html from " ++ url ++ ">")
main :: IO ()
main = do
pages <- mapConcurrently fetch ["a.com", "b.com", "c.com"]
mapM_ putStrLn pages
(x, y) <- concurrently (fetch "left.com") (return (length [1 .. 1000 :: Int]))
print (length x, y)
winner <- race (threadDelay 50000 >> return "slow") (return "fast")
print winner
a <- async (return (sum [1 .. 100 :: Int]))
wait a >>= print
<html from a.com>
<html from b.com>
<html from c.com>
(20,1000)
Right "fast"
5050
mapConcurrentlyruns one thread per element and returns results in order;concurrentlyruns two actions at once.racereturns whichever action finishes first (LeftorRight) and cancels the other.- If any thread throws, the exception propagates to the caller and the other threads are cancelled — no silent failures.
Channels
Chan (in Control.Concurrent.Chan) is an unbounded queue for
producer/consumer designs; the stm package provides bounded queues
(TBQueue) as well.
Using all your cores
Compile with -threaded and run with +RTS -N (or build with
-with-rtsopts=-N) so the runtime uses every CPU core. For pure
parallel computation (rather than concurrent I/O), the parallel
package's parMap and strategies split work across cores without any
threads or locks in your code.