exceptionhaskellconcurrencyfunctional-programming

Why should bracket's first argument perform at most one blocking operation?


In Parallel and Concurrent Programming in Haskell by Simon Marlow, the implementation of bracket is shown,

bracket
        :: IO a         -- ^ computation to run first (\"acquire resource\")
        -> (a -> IO b)  -- ^ computation to run last (\"release resource\")
        -> (a -> IO c)  -- ^ computation to run in-between
        -> IO c         -- returns the value from the in-between computation
bracket before after thing =
  mask $ \restore -> do
    a <- before
    r <- restore (thing a) `onException` after a
    _ <- after a
    return r

and a commentary follows. Here's the part that is puzzling me

[…] It is normal for before to contain a blocking operation; if an exception is raised while before is blocked, then no harm is done. But before should perform only one blocking operation. An exception raised by a second blocking operation would not result in after being executed. […]

I don't quite understand that comment, so I'd like some clarification about it.

To be clear, I don't understand even the no harm is done part: a few pages earlier (p. 159), the following failed (second) attempt at calling takeMVar, perform an operation depending on its content, and finally putting the result of that operation back in the MVar via putMVar is shown,

problem :: MVar a -> (a -> IO a) -> IO ()
problem m f = mask $ \restore -> do
  a <- takeMVar m
  r <- restore (f a) `catch`  \e -> do putMVar m a; throw e
  putMVar m r

In looking at this example, I "accept" as a fact that takeMVar does not tamper with m until it returns; indeed, this is taken from the text:

it would be safe for exceptions to be raised right up until the point where takeMVar returns.

and I feel like I understand what follows (I think).

But going back to the implementation of bracket, what if before internally uses takeMVar, and an asynchronous exception strikes after that takeMVar has returned (thus emptying an MVar)? Is this a no-problem for the very reason that we are using mask+restore, i.e. such exception would be delayed until the argument to restore, which is thing a, starts executing, at which point the necessary exception handler is in place, in this case via `onException` after a?

And what goes wrong if before makes another call to blocking operation, say takeMVar again for simplicity? Does the problem occur because the exception could strike while takeMVar is blocking, hence in the time window in which exceptions are not masked, so the exception would bubble out of bracket, leaving the MVar that was argument to the first takeMVar not empty, yet in a state which was not the original state, given after has not had a chance to run?

Is this it?

Furthermore, the doc page does not make any mention of this, or I don't see how it is implied.


Solution

  • Imagine that, in the before argument to bracket, you tried to open two different files, one after the other. (And also, correspondingly, that you tried to close them both in the after argument).

    If the attempt to open the first file fails for whatever reason, no handle is allocated at all, and there's nothing to clean up.

    But suppose the first file is opened successfully, but the attempt to open the second file fails (perhaps by itself, perhaps by some asynchronous exception that is received while blocked). Because after is not called for exceptions raised during before, the handle for the first file will remain open.

    what if before internally uses takeMVar, and an asynchronous exception strikes after that takeMVar has returned (thus emptying an MVar)? Is this a no-problem for the very reason that we are using mask+restore, i.e. such exception would be delayed until the argument to restore, which is thing a, starts executing, at which point the necessary exception handler is in place, in this case via onException after a?

    This is correct. When we are in a "masked" state, asynchronous exceptions can only happen when an interruptible operation (like takeMVar) blocks.

    As the "Masking Asynchronous Exceptions" chapter of the book mentions:

    Think of mask as “switching to polling mode” for asynchronous exceptions. Inside a mask, asynchronous exceptions are no longer asynchronous, but they can still be raised by certain operations. In other words, asynchronous exceptions become synchronous inside mask.