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 whilebefore
is blocked, then no harm is done. Butbefore
should perform only one blocking operation. An exception raised by a second blocking operation would not result inafter
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.
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.