Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell exceptions what mask/restore is particulary doing

I'm trying to understand how masking exceptions works in general case on a simple sample code, here is the bracket fn in library sources:

    bracket before after thing =
      mask $ \restore -> do
        a <- before
        r <- restore (thing a) `onException` after a
        _ <- after a
        return r

Here is my "bracket" version without mask

    myBracket before after thing = do
      a <- before
      r <- thing a `onException` after a
      _ <- after a
      pure r

I'm testing it with this simple code like this:

   tid <- forkIO $ bracket acquire release work
   treadDelay some
   throwTo tid MyException

I throw custom exceptions at different moments: when acquire, work, and release are executed, and for both bracket and myBracket I get the same results.

So my question is how do I modify my sample/testing code so that the role of mask/restore in exception handling is visible/obvious?

like image 401
WHITECOLOR Avatar asked Oct 24 '25 16:10

WHITECOLOR


2 Answers

Quoting from "Parallel and Concurrent Programming in Haskell":

a small number of operations, including takeMVar, are designated as interruptible. Interruptible operations may receive asynchronous exceptions even inside mask.

What justifies this choice? 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.

In your mask-using bracket, the before and after actions might still throw an asynchronous exception if they happen to use interruptible operations like takeMVar, threadDelay, or hClose. Even inside mask! In general, IO operations that might make the user wait for long periods of time tend to be interruptible.

What mask ensures is that asynchronous exceptions won't happen in the interstices between IO actions, or for IO actions that can't cause long waits. This make asynchronous exceptions much more manageable. You can, for example, handle them with a try, without fearing that the try itself might get interrupted.1

In your version of bracket without mask, an asynchronous exception might crop up between the execution of before and thing, or between the execution of thing and after. In both cases, after won't get executed and the bracketed resource will remain unreleased.

1That said, any async exception that is caught should eventually be re-thrown.


Note that, according to the above, if you pass something like threadDelay 10000000 as before, it might totally throw an asynchronous exception, even inside mask! It's your responsability to pass before actions (that is, allocation actions) that don't leave unreleased resources hanging when interrupted.

Functions like openFile do the right thing in those cases, and this, plus the knowledge that bracket is properly masked, means the user doesn't have to worry.


By the way: we said that hClose is interruptible. Should be worried about interrupted brackets leaving unclosed handles? No, because the contract for hClose says:

hClose is an interruptible operation in the sense described in Control.Exception. If hClose is interrupted by an asynchronous exception in the process of flushing its buffers, then the I/O device (e.g., file) will be closed anyway.

So, hClose will close the handle even if interrupted.


This piece of code uses uninterruptibleMask_ (a function which should be used with great care due to its propensity to make programs unresponsive) to create an un-interruptible threadDelay which is passed to bracket.

The masked version of bracket will print "after". The mask-less version won't.

This is because, without mask, the exception thrown to the thread will surface immediately after before finishes, without giving time to install the onException handler which ensures cleanup.

In contrast, for the masked version of bracket, the exception doesn't surface until we call the restore callback. But at that point the onException handler has already been installed.

main :: IO ()
main = do
    tid <- forkIO $
        bracket 
        -- myBracket 
            (uninterruptibleMask_ $ threadDelay 9000000) 
            (\_ -> hPutStrLn stderr "after")
            (\_ -> threadDelay 1000000) 
    threadDelay 2000000
    -- throwTo blocks until "before" finishes because the uninterruptible mask
    throwTo tid (userError "boo") 
    threadDelay 1000000
    pure ()
like image 188
danidiaz Avatar answered Oct 27 '25 17:10

danidiaz


If the exception gets thrown during acquire, then we may never release if there's no masking. Here's a crisp demonstration:

big = 100000000
acquire n = putStrLn "acquire start" >> evaluate (last [0..n]) >> putStrLn "acquire stop"
release _ = putStrLn "release"
work _ = putStrLn "work"

main = do
  tid <- forkIO $ myBracket (acquire (2*big)) release work
  evaluate (last [0..big])
  throwTo tid MyException
  threadDelay 1000000 -- so prints don't overlap

  tid <- forkIO $ bracket (acquire (2*big)) release work
  evaluate (last [0..big])
  throwTo tid MyException
  threadDelay 1000000 -- so the program doesn't exit until prints are done

When run, we can see that release only prints the second time, for bracket:

% ghc test && ./test
Loaded package environment from /home/dmwit/.ghc/x86_64-linux-8.10.4/environments/default
[1 of 1] Compiling Main             ( test.hs, test.o )
Linking test ...
acquire start
test: MyException
acquire start
acquire stop
release
test: MyException
like image 35
Daniel Wagner Avatar answered Oct 27 '25 15:10

Daniel Wagner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!