Haskell Concurrency Guide — ForkIO, MVars, STM, and Async
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