3

Browsing the source of readChan one finds the following implementation and comment, starting with version 4.6 of base:

-- |Read the next value from the 'Chan'.
readChan :: Chan a -> IO a
readChan (Chan readVar _) = do
  modifyMVarMasked readVar $ \read_end -> do -- Note [modifyMVarMasked]
    (ChItem val new_read_end) <- readMVar read_end
        -- Use readMVar here, not takeMVar,
        -- else dupChan doesn't work
    return (new_read_end, val)

-- Note [modifyMVarMasked]
-- This prevents a theoretical deadlock if an asynchronous exception
-- happens during the readMVar while the MVar is empty.  In that case
-- the read_end MVar will be left empty, and subsequent readers will
-- deadlock.  Using modifyMVarMasked prevents this.  The deadlock can
-- be reproduced, but only by expanding readMVar and inserting an
-- artificial yield between its takeMVar and putMVar operations.

Prior to base version 4.6, modifyMVar was used rather than modifyMVarMasked.

I don't understand what theoretical problem is solved for here. The last sentence states there is a problem if the thread yields between the takeMVar and putMVar that comprise readMVar. But as readMVar executes under mask_, how can an async exception prevent the put after successful take?

Any help understanding the issue here is appreciated.

wrl
  • 131
  • 4

3 Answers3

1

Let's compare the source of modifyMVar and modifyMVarMasked, since the code changed from using one to using the other:

modifyMVar m io =
  mask $ \restore -> do
    a      <- takeMVar m
    (a',b) <- restore (io a) `onException` putMVar m a
    putMVar m a'
    return b

modifyMVarMasked m io =
  mask_ $ do
    a      <- takeMVar m
    (a',b) <- io a `onException` putMVar m a
    putMVar m a'
    return b

The key here is that modifyMVar calls restore before executing its second argument, whereas modifyMVarMasked does not. So readMVar was not called under mask_ in the old version of the code as you claim in your question! It was called under restore, instead, and therefore asynchronous exceptions could be enabled after all.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • 1
    Disclaimer: I'm very much not an expert, and I've been wrong about concurrency in many languages many times before. – Daniel Wagner Feb 13 '14 at 02:43
  • don't feel too badly, everyone's been wrong about concurrency many times. – John L Feb 13 '14 at 03:06
  • @DanielWagner but `readMVar` re-masks exceptions, no? That explanation in the comment seems fishy to me – jberryman Feb 13 '14 at 03:33
  • @DanielWagner Hi Daniel. Thanks for the idea, but I was actually referring to the mask_ in the implementation of readMVar. I'm still not clear as to when an async exception could arrive that could lead to deadlock since read_end will always be "put" after a successful "take", and modifyMVar (even in non-masked form) will putMVar the head back to its original value on async exception. From Control.Exception "There's an implied mask around every exception handler in a call to one of the catch family of functions." – wrl Feb 13 '14 at 05:34
0

Here's me working through it.

So in readMVar...

readMVar :: MVar a -> IO a
readMVar m =
  mask_ $ do
    a <- takeMVar m
    putMVar m a
    return a

...despite the mask_ the runtime may raise an exception in a blocked takeMVar. Note in that function there's no need to actually handle that case; either the readMVar worked, in which case we're safe from async exceptions, or the takeMVar never succeeds; either way we never break the MVar by leaving it empty. (Is this correct? This is what I took away from the answer to my own related question.)

modifyMVar and modifyMVarMasked are:

modifyMVar :: MVar a -> (a -> IO (a,b)) -> IO b
modifyMVar m io =
  mask $ \restore -> do
    a      <- takeMVar m
    (a',b) <- restore (io a) `onException` putMVar m a
    putMVar m a'
    return b

modifyMVarMasked :: MVar a -> (a -> IO (a,b)) -> IO b
modifyMVarMasked m io =
  mask_ $ do
    a      <- takeMVar m
    (a',b) <- io a `onException` putMVar m a
    putMVar m a'
    return b

...where the difference is in modifyMVar the masking state is restored (i.e. async exceptions probably become unmasked) in io a, which in our case is more or less readMVar.

EDIT: Although readMVar is mask_-ed as well, so now I can't see why either choice of modifyMVarMasked or modifyMVar would make a difference...

The comment seems to imply that yield (inserted into readMVar) is interruptible (I can't find this documented anywhere) and so an async exception might be raised, in which case readVar would be restored (in both current and pre-4.6 versions), but in a non-empty queue readers would see an empty one and block.

Community
  • 1
  • 1
jberryman
  • 16,334
  • 5
  • 42
  • 83
  • That is my read too. It implies something special about yield but its unclear what that could be. And if there is some theoretical problem here wouldn't that same problem exist with every usage of readMVar? As for other readers of a non-empty queue, shouldn't they be blocked on the outer readVar? If an async exception is thrown to one reader that just got a value, the `onException` in modifyMVar (masked or not) will fire and restore readVar to the previous head. So I'm still stumped about where the problem is that modivyMVarMasked solves. – wrl Feb 13 '14 at 04:21
  • @wrl yeah, after several rethinks it seems like the only difference would be the async exception raised within `restore` but before the `mask_` in readMVar is entered, but I don't see how `yield` would matter there. – jberryman Feb 13 '14 at 04:48
  • I doubt there's anything special about yield, rather I think that inserting any interruptible op between the `takeMVar` and `putMVar` would allow the deadlock to be reproduced. But without inserting an interruptible op, the deadlock won't actually be possible. – John L Feb 13 '14 at 05:36
  • @JohnL That makes sense, any interruptible operation that got inserted there could lead to deadlock. But how does using modifyMVarMasked solve this? As I understand the only way to mask async exceptions in an interruptible call is to use uninterruptibleMask. – wrl Feb 13 '14 at 07:02
  • @wrl I don't think `modifyMVarMasked` does solve it. I think the committer overlooked that `readMVar` does its own `mask_` (or perhaps that was added later). But I don't think it's a real problem, because it seems very unlikely that somebody will add an interruptible op into the middle of `readMVar`. – John L Feb 13 '14 at 07:16
0

You may be interested in reading the GHC trac on this commit, which has a sample program that consistently reproduces this bug when both Control.Concurrent.Chan and the test program are compiled -O0

https://ghc.haskell.org/trac/ghc/ticket/6153

In a similar vein:

https://ghc.haskell.org/trac/ghc/ticket/5870

lpsmith
  • 707
  • 3
  • 8