readMVar doesn't wake up on putMVar

Viewed 108

My code seems to be hanging on a readMVar after another thread calls putMVar. I wouldn't expect this to happen, but that's what I'm observing. My main thread creates two new threads, each with access to a shared MVar m.

Thread 1:

do
  putStrLn "tick"
  x <- readMVar m
  putStrLn "tock"

Thread 2:

do
  putMVar m 0
  putStrLn "put m0"
  void $ tryTakeMVar m
  putStrLn "take m"
  putMVar m 1
  putStrLn "put m1"

Main:

do
  m <- newEmptyMVar
  <start thread 1>
  <start thread 2>

In the following scenario, my program hangs:

Two threads have access to a shared MVar m, which is initially empty. Thread 1 blocks on readMVar m. Meanwhile, thread 2 calls putMVar m .... At this point, thread 1 could proceed, but let's suppose it does not. Thread 2 then calls tryTakeMVar m, which presumably empties a full MVar. Then thread 2 again calls putMVar m .... This scenario corresponds to the following output:

tick
put m0
take m
put m1
<hang>

What's going on here? I expect that "tock" should print, since thread 2 filled the MVar, but my program just hangs.

1 Answers

I switched my MVar implementation from base to strict-concurrency while trying to debug a space leak. But as the question indicates, my code uses tryReadMVar, which is for some reason not provided by strict-concurrency. Thus, a while back, I implemented tryReadMVar myself like this:

tryReadMVar :: (NFData a) => MVar a -> IO (Maybe a)
tryReadMVar m = do
  mm <- tryTakeMVar m
  case mm of
    Nothing -> return ()
    Just a -> putMVar m a
  return mm

without really considering the implications. I had since forgotton all about doing this. As Daniel pointed out, old versions of base used to do something similar, but newer versions have an atomic tryReadMVar implementation. So even though I was using a new version of GHC, the problem was reintroduced as a result of using strict-concurrency.

Concurrently, the deadlock occurred in the following situation (which Daniel describes):

  • thread 1 prints "tick"
  • thread 2 puts the mvar using putMVar
  • thread 2 prints "put m0"
  • thread 1 takes the mvar using tryTakeMVar within tryReadMVar
  • thread 2 takes the mvar using tryTakeMVar
  • thread2 prints "take m"
  • thread2 puts the mvar using putMVar
  • thread2 prints "put m1"
  • thread 1 deadlocks while trying to putMVar within tryReadMVar

Turns out that having an atomic tryReadMVar is useful!

Related