Beware of bracket
July 30, 2014
Many Haskellers know and love the
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
In our volatile world
bracket provides a safe haven of certainty: the resource will be released, no matter what. However, there’s a catch. (Nice pun, huh?)
This article describes two observations about
bracket that are useful to keep in mind when writing concurrent Haskell code.
Is the resource you’re trying to release something internal to your code, or is observable outside?
In the former case, you’re fine. An example would be taking an
MVar and putting it back afterwards:
withMVar :: MVar a -> (a -> IO b) -> IO b withMVar m io = bracket (takeMVar m) (putMVar m) io
Now consider the case when the resource is something tangible — let’s say, a directory.
withFoo :: IO a -> IO a withFoo io = bracket_ (createDirectory "foo") (removeDirectoryRecursive "foo") io
You might think that there’s no way for
foo to remain in the filesystem after the program is done, barring OS-level issues.
In reality, you only get this guarantee if
withFoo is executed in the main Haskell thread (i.e. the thread that executes
main). When the main thread finishes, the program finishes, too, without sending exceptions to other threads and giving them any chance to finalize.
This limitation is especially acute in library code, where you don’t know whether you’re running in the main thread or have any way to tie to it.
You could try to write something like
main = do bracket (forkIO myComputation) killThread $ \_ -> do ...
The idea here is that if
main exits, for whatever reason, you’ll send an exception to the thread you forked, and give it a chance to clean up.
First, this isn’t going to help much because
main will exit right after
killThread, probably right in the middle of
myComputation’s cleanup process. Some kind of synchronisation should be introduced to address this properly. (The price is that your program may not exit promptly when you interrupt it with Ctrl-C.)
There’s another, more subtle issue with the code above. Let’s look at the definition of
bracket before after thing = mask $ \restore -> do a <- before r <- restore (thing a) `onException` after a _ <- after a return r
As you see, the
before action is run in the masked state. Forked threads inherit the masked state of their parents, so
myComputation and all threads spawned by it will unwittingly run in the masked state, unless they do
In this simple case, you should just use
withAsync from the async package. What about more complex ones?
If you do forking explicitly, then you can write
bracket-like code yourself and restore the forked computation. Here’s an example of synchronised cleanup:
main = do cleanupFlag <- atomically $ newTVar False mask $ \restore -> do pid <- forkIO $ restore $ myComputation cleanupFlag restore restOfMain `finally` do killThread pid -- wait until myComputation finishes its cleanup -- and sets the flag to True atomically $ readTVar cleanupFlag >>= check
(You could use an
MVar for synchronisation just as well.)
And what if forking happens inside some library function that you need to call? In that case, you may want to
restore that whole function from the beginning.