For myself, here is 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
We have a background thread and a main thread that performs simultaneous transmission through a small piece of memory, MVar , called m .
MVar semantics are: a MVar can be empty or full. If you want to read MVar and it is empty, you have to wait until it is full. If you readMVar , then you simply enable the value stored in full MVar as soon as you can. If you takeMVar , then you decide the value, and then immediately clear it after reading.
On the other hand, when you putMVar to put a new value in MVar , you will immediately succeed if MVar empty. If it is full, you must wait until it becomes empty.
Since there is a wait on the read and write sides, the threads are synchronized in the emptiness and completeness of MVar .
So, in this example, we can present many possible linearized stories about how execution is performed. Fortunately, they all work the same way. Let the BG background thread and the main thread MN called.
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 cannot put more values ββin MVar than it can read MN . This gives the printed semantics that you have observed.