Skip to content
AgileLogics
8 min read

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
  • forkIO starts a new thread running an IO action.
  • An MVar is a box that is either empty or full. takeMVar waits until it's full and empties it; putMVar waits 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
  • mapConcurrently runs one thread per element and returns results in order; concurrently runs two actions at once.
  • race returns whichever action finishes first (Left or Right) 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.