Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

`forkIO` and `putMVar`: what's going on under the hood?

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'
like image 921
iceman Avatar asked Nov 11 '14 01:11

iceman


1 Answers

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.

like image 68
J. Abrahamson Avatar answered Sep 18 '22 21:09

J. Abrahamson