Skip to content

Haskell Concurrency Guide — ForkIO, MVars, STM, and Async

DodaTech Updated 2026-06-28 3 min read

In this tutorial, you will learn about Haskell Concurrency Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Haskell concurrency provides lightweight green threads with forkIO, mutable state through MVars and IORefs, composable atomic transactions via STM (retry, orElse), and high-level async/await patterns for parallel task management.

ForkIO

import Control.Concurrent
import Control.Monad (forever)

-- Spawn a lightweight thread
main = do
    forkIO $ forever $ do
        threadDelay 1000000  -- 1 second
        putStrLn "Tick"
    
    threadDelay 5000000
    putStrLn "Done"

MVars

import Control.Concurrent

-- MVar: mutable box that can be empty or full
-- putMVar blocks if full, takeMVar blocks if empty

main = do
    mvar <- newEmptyMVar
    
    forkIO $ do
        putStrLn "Worker starting..."
        threadDelay 2000000
        putMVar mvar "Result data"
    
    putStrLn "Waiting for result..."
    result <- takeMVar mvar
    putStrLn $ "Got: " ++ result

STM (Software Transactional Memory)

import Control.Concurrent.STM

-- STM: composable atomic transactions

type Account = TVar Int

createAccount :: Int -> IO Account
createAccount balance = newTVarIO balance

transfer :: Account -> Account -> Int -> STM ()
transfer from to amount = do
    fromBalance <- readTVar from
    toBalance   <- readTVar to
    
    -- Retry if insufficient funds
    check (fromBalance >= amount)
    
    writeTVar from (fromBalance - amount)
    writeTVar to   (toBalance + amount)

-- Atomic execution
atomically $ transfer acc1 acc2 100

Async

import Control.Concurrent.Async

-- Run two tasks concurrently
main = do
    result1 <- async $ longComputation 1
    result2 <- async $ longComputation 2
    
    -- Wait for both
    r1 <- wait result1
    r2 <- wait result2
    
    print (r1, r2)

-- Concurrent map
concurrentMap :: (a -> IO b) -> [a] -> IO [b]
concurrentMap f xs = do
    asyncs <- mapM (async . f) xs
    mapM wait asyncs

-- With timeout
import System.Timeout
timeout 1000000 $ someComputation

Common Mistakes

1. Shared mutable state without MVars

IORefs are not thread-safe without atomicModifyIORef. Use MVars, TVars, or atomicModifyIORef for shared state.

2. STM retry without orElse

retry blocks until a watched variable changes. Use orElse for fallback: transfer a b 100 orElse transfer b a 50.

3. Forking without Exception Handling

forkIO threads that throw exceptions die silently. Use catch in the forked thread or supervise with Async.

Practice Questions

1. What is the difference between MVar and TVar? MVar is a single-slot channel (blocking). TVar supports transactional composition with retry and orElse.

2. What does STM retry do? Retries the Transaction, blocking until any variable read during the transaction is modified.

3. How do you run two tasks concurrently and wait for both? Use async from Control.Concurrent.Async: async task1 >>= \a1 -> async task2 >>= \a2 -> wait a1 >> wait a2.

FAQ

{{< faq question="Are Haskell threads OS threads?" >}} No. forkIO creates lightweight green threads managed by the RTS. They multiplex onto a small number of OS threads. {{< /faq >}}

{{< faq question="What is the advantage of STM over locks?" >}} STM is composable. You can combine two atomic operations into a larger atomic one. Locks don't compose. {{< /faq >}}

{{< faq question="How do I set the number of OS threads?" >}} Compile with -threaded and run with +RTS -N4 -RTS for 4 OS threads, or -N for all cores. {{< /faq >}}

What's Next

Now learn about the FFI (Foreign Function Interface).

Topic Description Link
FFI Calling C from Haskell {{< ref "22-ffi" >}}
Profiling Performance profiling {{< ref "23-profiling" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro