I am hoping someone can help me understand why the following code generates the output below. The code is from the Concurrency chapter in Simon Marlow's book (link below).
Based on the description of the various functions, I assumed that the second putMVar
function should get blocked given that (i) both putMVar
functions are part of the same thread and (ii) a value has already been assigned. Clearly that's not the case. It would be great to understand what's going on 'under the hood' here.
(Note: the book uses do
notation but I have a preference for >>=
notation as I think it's more straightforward - hence the version of the code below.)
Link to book
import Control.Concurrent
main :: IO ()
main = newEmptyMVar >>=
\m -> forkIO (putMVar m 'x' >>= \_ -> putMVar m 'y') >>=
\_ -> takeMVar m >>=
print >>=
\_ -> takeMVar m >>=
print
Output of the code above:
% ./mvar2
'x'
'y'
For my own sake, here's the code in do
notation.
main :: IO ()
main = do
m <- newEmptyMVar
forkIO $ do
putMVar m 'x'
putMVar m 'y'
x <- takeMVar m
print x
y <- takeMVar m
print y
What we have is a background thread and the main thread running concurrently communicating over a small piece of memory, the MVar
called m
.
MVar
semantics are as such: an MVar
can be empty or full. If you want to read an MVar
and it is empty then you must wait until it become full. If you readMVar
then you will simply resolve the value stored in a full MVar
as soon as you can. If you takeMVar
then you will resolve the value and then immediately empty it after reading.
On the other side, when you putMVar
to put a new value into an MVar
you will succeed immediately if the MVar
is empty. If it is full then you must wait until it becomes empty.
Since there is waiting on the read and the write sides then the threads become synchronized over the emptiness and fullness of the MVar
.
So in this example, we can imagine many possible linearized stories for how the execution goes forward. Fortunately, they all work identically. Let's call the background thread BG
and the main thread MN
.
t = 1 : MN makes a new, empty MVar called 'm'
t = 2 : BG puts 'x' in 'm' making it full
t = 3 : BG attempts to put 'y' in 'm', but since 'm' is full BG blocks
t = 4 : MN attempts to read 'm' and succeeds as it is full
t = 5 : BG now places 'y' into the newly empty 'm'
t = 6 : BG dies
t = 6 : MN prints the value it previously read
t = 7 : MN attempts to read 'm' and succeeds as it is full
t = 8 : MN prints the value it previously read
t = 9 : MN dies
As we can see, BG
is prevented from putting more values in the MVar
than what MN
can read. This produces the printed semantics you observed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With