4
votes

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
What version of GHC? In old ones, readMVar is a takeMVar followed by a putMVar (not performed atomically). If the tryTakeMVar and second putMVar happened between those, it would explain the behavior you're seeing. - Daniel Wagner
I believe GHC 8.6.3, though it's a bit hard to tell when using Stack. Given your clean explanation, however, I'll try to dig a bit more into which version is being used. - crockeea
Confirmed that it's GHC 8.6.3. - crockeea
Here's the problem: I'm using strict-concurrency, which doesn't provide tryReadMVar. As a result, I implemented it myself using tryTakeMVar and putMVar (non-atomically). Thus what Daniel says is spot on. - crockeea
You should write that up as an answer. That's good work tracking it down. - Carl

1 Answers

5
votes

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!