The blog post introduces how we can achieve concurrency in Haskell.

You might notice that it has been almost 3 weeks since I published an article on Haskell. The reason for that gap is not because I lost interest in Haskell. It is becuase I wanted to cover the basics concurrency and parallelism in C so that I can focus on their implementations in Haskell. Hence, if you want to learn the concept more conceptually, I would recommend checking the article, "Road to C Programmer #13 - Multithreading".
Threads
To achieve concurrency or parallelism, we need to implement multithreading. To create a new thread in Haskell, you can use the following:
import Control.Concurrent
-- Create new thread
-- forkIO :: IO() -> IO ThreadId
f :: Int -> Int -> IO ()
f a b = do
let x = a + b
putStrLn $! show x
main :: IO ()
main = do
...
forkIO $ f 1 2
...
By using forkIO
function from Control.Concurrent
module, you can create a new thread. The forkIO
function
can take an IO action as an input and return an IO action with ThreadId
, which can be used to identify the
thread. The above uses $!
in f
function as it might return a thunk and fail to utilize processors
without it due to default lazy evaluation in Haskell.
Also, Haskell is not multithreaded and only utilizes single processor by default, so you will need to compile
the code with -threaded
tag like ghc -threaded main.hs
to enable multithreading, and you will need to
execute the executable with -N
like ./main +RTS -N
to access multiple processors in your machine.
Mutex Variables
To have an access to the result of a function in another thread, MVar
. It works exactly like a mutex
lock in C, but with storage capability. A value can be put to and taken from a mutex variable only one at a time.
import Control.Concurrent
f :: Int -> Int -> MVar Int -> IO ()
f a b mVar = do
putMVar mVar $! (a + b) -- put value in MVar
main :: IO ()
main = do
mVar <- newEmptyMVar -- initialize MVar
forkIO $ f 1 2 mVar
result <- takeMVar mVar -- take value of MVar
putStrLn $ show result
The takeMVar
function can be triggered only after a value is put in the mVar
with the putMVar
function.
Channels
Channels, Chan
, is another way of sharing the results of functions in other threads. Unlike MVar
,
threads can put as many values as possible in a channel, but taking them requires that there is a value
stored in the channel. The value is taken from the values that are put first, making it a FIFO (first in first out)
queue.
import Control.Concurrent
f :: Int -> Int -> Chan Int -> IO ()
f a b chan = do
writeChan chan $! (a + b) -- write value in Chan
main :: IO ()
main = do
chan <- newChan -- initialize Chan
forkIO $ f 1 2 chan
forkIO $ f 3 4 chan
result1 <- readChan chan -- read value in Chan
result2 <- readChan chan -- read value in Chan
putStrLn $ show result1 -- => (3 or 7)
putStrLn $ show result2 -- => (3 or 7)
When the order of reading the result do not matter, it might be more effective to use channels as it can
easily facilitate parallel execution. The readChan
will only read when there is a value already stored
in a channel using writeChan
.
MVar & Chan
Let's see them more in action. The below code tries to create 10 new threads that prints out their thead ids.
getGreeting :: IO String
getGreeting = do
tid <- myThreadId -- thread id can be retrieved like this
let greeting = "Hello from" ++ show tid
return $! greeting
hello :: IO ()
hello = do
greeting <- getGreeting
putStrLn greeting
main :: IO()
main = do
let n = 10
mapM_ (const $ forkIO $ hello) [1..n]
return ()
However, the above has a critical flaw that it sometimes only prints some ids but not all of them.
It is because return ()
does not wait until all the threads finish their execution and terminate
the entire program. To make sure that we can wait for all of the threads to finish their execution,
we can create a channel that stores the empty tuples and read the channel ten times before running
return ()
.
getGreeting :: IO String
getGreeting = do
tid <- myThreadId
let greeting = "Hello from" ++ show tid
return $! greeting
hello :: Chan () -> IO ()
hello endFlags = do
greeting <- getGreeting
putStrLn greeting
writeChan endFlags ()
main :: IO()
main = do
endFlags <- newChan
let n = 10
mapM_ (const $ forkIO $ hello endFlags) [1..n]
mapM_ (const $ readChan endFlags) [1..n]
The above code waits until readChan
reads 10 empty tuples from endFlags
channel before exiting
the program. Hence, you can see all the threads executing every time you run the above. However,
the above is executing fine because Haskell protects standard output with a buffer by default.
When you disable the buffer by importing import System.IO
and putting hSetBuffering stdout NoBuffering
inside of the main
IO action, you would see that the characters are put immediately during the executions
of putStrLn
in multiple threads in parallel, which results in standard output looking like:
HeHHHHHHHHHleeeeeeeeellllllllllolllllllll ooooooooof rffffffffforrrrrrrrrmooooooooommTmmmmmmmTThTTTTTTThhrhhhhhhhrrerrrrrrreeaeeeeeeeaadaaaaaaaddIdddddddIIdIIIIIIIdd ddddddd 1 1115111122296
478310
To fix this problem, we can use MVar
and lock the putStrLn
operation exactly like a mutex lock.
getGreeting :: IO String
getGreeting = do
tid <- myThreadId
let greeting = "Hello from" ++ show tid
return $! greeting
hello :: MVar () -> Chan () -> IO ()
hello mutexLock endFlags = do
greeting <- getGreeting
takeMVar mutexLock
putStrLn greeting
putMVar mutexLock ()
writeChan endFlags ()
main :: IO()
main = do
hSetBuffering stdout NoBuffering
mutexLock <- newEmptyMVar
endFlags <- newChan
let n = 10
mapM_ (const $ forkIO $ hello mutexLock endFlags) [1..n]
putMVar mutexLock ()
mapM_ (const $ readChan endFlags) [1..n]
Now, when you run the above, you should see a sane output like the below:
Hello fromThreadId 14
Hello fromThreadId 13
Hello fromThreadId 16
Hello fromThreadId 12
Hello fromThreadId 15
Hello fromThreadId 18
Hello fromThreadId 19
Hello fromThreadId 21
Hello fromThreadId 20
Hello fromThreadId 17
You need to be careful where you create a lock, as you might not be able to take advantage of parallelism.
Exercises
This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.
Resources
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #28 - Concurrency & Threads. YouTube.