## Async exception handling in Haskell
* Michael Snoyman
* VP of Engineering
* FP Complete webinar
* April 11, 2018
<div style="text-align:center">
<div><img src="/static/fpcomplete-logo.png" style="border:0;margin:0"></div>
</div>
---
## From the beginning
* Many languages have synchronous exceptions
* Double-edged sword
* Arguably easier to write correct code
* Can lead to lack of resource cleanup
* GHC Haskell has asynchronous exceptions too
* Let's just say "Haskell," you know what I mean now
* Gotta cover exceptions to get to async exceptions
----
## What we'll cover today
* Defining different types of exceptions
* Correct synchronous exception handling
* How bottom values play in
* Basics of async exceptions
* Masking and uninterruptible masking
* Helper libraries
* Some more complex examples
Lots of ground to cover before we talk about async stuff!
----
## Fear is the mind-killer
* Async exceptions _are_ tricky
* They aren't nearly as terrifying as lore makes them out as
* Usually: use the right helper library, everything's good
----
## Are exceptions good or bad?
* Not our topic today!
* Lots of healthy debate inside and outside the Haskell community
* However: runtime exceptions are the reality of GHC Haskell today
* Whether you like it or not: need to deal with it
----
## Teaser
Goal for this talk: you should see multiple reasons I call this
function `badRace`:
```haskell
badRace :: IO a -> IO b -> IO (Either a b)
badRace ioa iob = do
mvar <- newEmptyMVar
tida <- forkIO $ ioa >>= putMVar mvar . Left
tidb <- forkIO $ iob >>= putMVar mvar . Right
res <- takeMVar mvar
killThread tida
killThread tidb
return res
```
---
## Motivating example
* Most complexity around scarce resource handling
* File handling great example, we'll use it
* Open the file, may fail
* Interact with the file handle, may fail
* Close the file handle regardless
* File descriptors are scarce!
* Start without any exceptions, build up from there
* Slight detour though...
----
## Pure code
* Cannot catch exceptions in pure code
* Makes sense: no resource allocation in pure code
* Except...
* Can throw from pure code 🙁
* Can use `unsafePerformIO` for allocations
* Memory can be allocated implicitly
* Not a contradiction! Memory ain't scarce
* Technically can use `unsafePerformIO` to catch
Overall: our focus is on non-pure, `IO` code. Slight reference to
transformers later.
----
## The land of no exceptions
Haskell without any runtime exceptions (great rejoicing in the land)
```haskell
openFile :: FilePath -> IOMode
-> IO (Either IOException Handle)
hClose :: Handle -> IO () -- assume it can never fail
usesFileHandle :: Handle -> IO (Either IOException MyResult)
myFunc :: FilePath -> IO (Either IOException MyResult)
myFunc fp = do
ehandle <- openFile fp ReadMode
case ehandle of
Left e -> return (Left e)
Right handle -> do
eres <- usesFileHandle handle
hClose handle
return eres
```
----
## Land of synchronous exceptions
Add two new primitives for synchronous exceptions
```haskell
throwIO :: IOException -> IO a
try :: IO a -> IO (Either IOException a)
```
__Synchronous exceptions are exceptions which are generated directly
from the `IO` actions you are calling.__
----
## Rewrite our function
```haskell
openFile :: FilePath -> IOMode -> IO Handle
hClose :: Handle -> IO ()
usesFileHandle :: Handle -> IO MyResult
myFunc :: FilePath -> IO MyResult
myFunc fp = do
handle <- openFile fp ReadMode
res <- usesFileHandle handle
hClose handle
return res
```
* Code is shorter
* Can't tell whether `openFile` and `hClose` can fail
* No need to pattern match on `openFile` result
* But wait! What if `usesFileHandle` throws an exception?
----
## Try and throw
Fix it!
```haskell
myFunc :: FilePath -> IO MyResult
myFunc fp = do
handle <- openFile fp ReadMode
eres <- try (usesFileHandle handle)
hClose handle
case eres of
Left e -> throwIO e
Right res -> return res
```
(Synchronous) exception safe!
----
## Capture the pattern
```haskell
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile fp mode inner = do
handle <- openFile fp mode
eres <- try (inner handle)
hClose handle
case eres of
Left e -> throwIO e
Right res -> return res
myFunc :: FilePath -> IO MyResult
myFunc fp = withFile fp ReadMode usesFileHandle
```
__General principle__: Avoid using functions which only allocate or
only clean up whenever possible.
__Question__: What if `cleanup` throws an exception?
---
## Extensible exceptions
* We assumed `IOException` above
* GHC has OO-style extensibility, like Java
* Please don't vomit
```haskell
data SomeException = forall e . Exception e => SomeException e
class (Typeable e, Show e) => Exception e where
toException :: e -> SomeException
fromException :: SomeException -> Maybe e
throwIO :: Exception e => e -> IO a
try :: Exception e => IO a -> IO (Either e a)
```
----
## Example exception, no hierarchy
```haskell
data InvalidInput = InvalidInput String
deriving (Show, Typeable)
instance Exception InvalidInput where
toException ii = SomeException ii
fromException (SomeException e) = cast e -- part of Typeable
```
`toException` and `fromException` have defaults, so...
```haskell
instance Exception InvalidInput
```
----
## Example of hierarchy
```haskell
data MyAppException
= InvalidInput String
| SomethingElse SomeException
deriving (Show, Typeable)
instance Exception MyAppException
```
```haskell
data SubException = NetworkFailure String
deriving (Show, Typeable)
instance Exception SubException where
toException = toException . SomethingElse . SomeException
fromException se = do
SomethingElse (SomeException e) <- fromException se
cast e
```
```haskell
main :: IO ()
main = do
e <- try $ throwIO $ NetworkFailure "Hello there"
print (e :: Either SomeException ())
```
---
## Exception in pure code
* Why call it `throwIO` and not `throw`?
```haskell
throw :: Exception e => e -> a
```
* Not an async exception!
* I call them __impure exceptions__
* Create bottom values
----
## Creating impure exceptions
* Using the `throw` function directly
* Using a function which calls `throw`, like `error`
* Using partial functions like `head`
* Incomplete pattern matches (GHC automatically inserts the equivalent
of a call to `throw`)
* Creating infinite loops in pure code, where GHC's runtime _may_
detect the infinite loop and throw a runtime exception
----
## Preaching to the choir
* Partiality is bad, m'kay?
* Avoid creating these impure exceptions
----
## Challenge: what's the output?
```haskell
import Control.Exception
import Data.Typeable
data Dummy = Dummy
deriving (Show, Typeable)
instance Exception Dummy
printer :: IO (Either Dummy ()) -> IO ()
printer x = x >>= print
```
```haskell
main :: IO ()
main = do
printer $ try $ throwIO Dummy
printer $ try $ throw Dummy
printer $ try $ evaluate $ throw Dummy
printer $ try $ return $! throw Dummy
printer $ try $ return $ throw Dummy
```
----
## Case 1
```
printer $ try $ throwIO Dummy
Left Dummy
```
We're using proper runtime exceptions via `throwIO`, and therefore
`Dummy` is thrown immediately as a runtime exception. Then `try` is
able to catch it, and all works out well.
----
## Case 2
```
printer $ try $ throw Dummy
Left Dummy
```
We generate a value of type `IO ()` which, when evaluated, will throw
a `Dummy` value. Passing this value to `try` forces it immediately,
causing the runtime exception to be thrown. The result ends up being
identical to using `throwIO`.
----
## Case 3
```
printer $ try $ evaluate $ throw Dummy
Left Dummy
```
`throw Dummy` has type `()`. The `evaluate` function then forces
evaluation of that value, which causes the `Dummy` exception to be
thrown.
----
## Case 4
```
printer $ try $ return $! throw Dummy
Left Dummy
```
This is almost identical; it uses `$!`, which under the surface uses
`seq`, to force evaluation. We're not going to dive into the
difference between `evaluate` and `seq` today.
----
## Case 5
```
printer $ try $ return $ throw Dummy
Right Main.hs: Dummy
```
* Odd man out
* Create thunk with `throw Dummy` of type `()`
* `return` wraps it into `IO ()`
* `try` forces evaluation of `IO ()`, which doesn't force evaluation of the `()`
* End up with value of type `Either Dummy ()`
* Equivalent to `Right (throw Dummy)`
* `printer` tries to print it, forces `throw Dummy`, causes crash
----
## What's the upshot?
* Not passing judgement, but: don't use `throw` and `error`
* If you use exceptions, use `throwIO`
* Pure exceptions seem to appear at "random"
* But the trigger for it getting thrown is always local
* Forcing evaluation inside `IO`
* Therefore, by our definition, impure exceptions _are_ synchronous
exceptions
* We'll treat them as such, but mostly just ignore them, because...
----
## Impure exceptions are irrelevant
Who cares if `inner` returns a partial/bottom value?
```haskell
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile fp mode inner = do
handle <- openFile fp mode
eres <- try (inner handle)
hClose handle
case eres of
Left e -> throwIO e
Right res -> return res
```
We never evaluate it in `withFile`, so it doesn't break anything
---
## Motivating async exceptions
We want a `timeout` function
```haskell
timeout :: Int -- microseconds
-> IO a -> IO (Maybe a)
```
Can we get something like this without async exceptions?
----
## Bad approach: built in primitive
Imagine: part of the runtime system, kills thread immediately
```haskell
timeout 1000000 $ bracket
(openFile "foo.txt" ReadMode)
hClose
somethingReallySlow
```
* `hClose` will never get called
* Defeats exception safety
----
## Outside the runtime (1)
```haskell
import Control.Concurrent (threadDelay, forkIO)
import Control.Concurrent.MVar
import Control.Exception
import Control.Monad (when, forever)
import Data.IORef
import Data.Typeable
data Timeout = Timeout
deriving (Show, Typeable)
instance Exception Timeout
type CheckTimeout = IO ()
```
----
## Outside the runtime (2)
```haskell
timeout :: Int -> (CheckTimeout -> IO a) -> IO (Maybe a)
timeout micros inner = do
retval <- newEmptyMVar
expired <- newIORef False
let checkTimeout = do
expired' <- readIORef expired
when expired' $ throwIO Timeout
_ <- forkIO $ do
threadDelay micros
writeIORef expired True
```
```haskell
_ <- forkIO $ do
eres <- try $ inner checkTimeout
putMVar retval $
case eres of
Left Timeout -> Nothing
Right a -> Just a
takeMVar retval
```
----
## Outside the runtime (3)
```haskell
myInner :: CheckTimeout -> IO ()
myInner checkTimeout = bracket_
(putStrLn "allocate")
(putStrLn "cleanup")
(forever $ do
putStrLn "In myInner"
checkTimeout
threadDelay 100000)
main :: IO ()
main = timeout 1000000 myInner >>= print
```
----
## Outside the runtime (4)
Positive: reuses existing exception machinery, so it's
safe. Negatives:
* Cannot interrupt pure code (`checkTimeout` is in `IO`)
* Have to remember to call `checkTimeout`, or `timeout` will break
__BONUS__ The code above has a potential deadlock in it due to mishandling of
synchronous exceptions. Try and find it!
---
## Real asynchronous exceptions
__Async exceptions are exceptions thrown from another thread.__
* Local thread does not cause the exception
* Bubble up just like synchronous exceptions
* Caught with `try` (and friends like `catch`)
* Difference is how they're thrown
```haskell
forkIO :: IO () -> IO ThreadId
throwTo :: Exception e => ThreadId -> e -> IO ()
```
----
## Compare to hand-written `timeout`
* `throwTo` like setting `expired` to `True`
* Runtime automatically calls `checkTimeout` equivalent
* Runtime __can detect async exception at any point__
* Can happen at unexpected times, leading to new problems
----
## The need for masking
Let's revisit our `withFile`, with explicit async-exception checking
calls
```haskell
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile fp mode inner = do
checkAsync -- 1
handle <- openFile fp mode
checkAsync -- 2
eres <- try (inner handle)
checkAsync -- 3
hClose handle
checkAsync -- 4
case eres of
Left e -> throwIO e
Right res -> return res
```
1 or 4: fine, 2 or 3: bad news bears
----
## The `mask_` function
Let's temporarily block async exceptions
```haskell
mask_ :: IO a -> IO a
withFile fp mode inner = mask_ $ ...
```
* Fixes the resource leak
* But now `timeout` can't kill `inner`!
* Need to restore the previous masking state
----
## The `mask` function
Mask, but get a function to restore
```haskell
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
```
__ADVANCED__ This restores instead of unmasking to deal with nested
maskings. This deals with the "wormhole" problem, which we won't
cover.
----
## Restore
```haskell
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile fp mode inner = mask $ \restore -> do
handle <- openFile fp mode
eres <- try (restore (inner handle))
hClose handle
case eres of
Left e -> throwIO e
Right res -> return res
```
* Safe to `restore`, because it's wrapped in a `try`
* No way for any exceptions to prevent `hClose`
---
## Catch 'em all!
* Code doesn't type check
* Which instance of `Exception`?
* Catch all exceptions with one weird trick
Replace
```haskell
eres <- try (restore (inner handle))
case eres of
Left e -> throwIO e
```
with
```haskell
eres <- try (restore (inner handle))
case eres of
Left e -> throwIO (e :: SomeException)
```
----
## Catch 'em all again
Code above is good, but what about this?
```haskell
main :: IO ()
main = do
start <- getCurrentTime
res <- timeout 1000000 $ do
x <- try $ threadDelay 2000000
threadDelay 2000000
return x
end <- getCurrentTime
putStrLn $ "Duration: " ++ show (diffUTCTime end start)
putStrLn $ "Res: " ++
show (res :: Maybe (Either SomeException ()))
```
----
## The problem
Output
```
Duration: 3.004385s
Res: Just (Left <<timeout>>)
```
* Duration is 3 seconds, not 1 second
* We get a `Just` instead of a `Nothing`
* Inside the `Just` is an exception from the timeout
We caught an async exception. Why is this bad here?
----
## Recover versus cleanup
__You cannot recover from an asynchronous exception__
Two reasons to catch an exception
* __Cleanup__: you catch, perform the cleanup action, and
rethrow. OK for async exception.
* __Recover__: catch the exception and continue with something else
without rethrowing. Not OK for async exception.
Recovering from async exception will break functions like `timeout`
---
## GHC's async exception flaw
* Difference between sync and async: `throwIO` vs `throwTo`
* When catching: no way to see which function threw it!
* Two different techniques to approximate
* Older technique: fork a thread
* Newer technique: rely on types
* We'll use the latter
* __WARNING__ It's possible to spoof this!
----
## SomeAsyncException
"Superclass" of all asynchronous exceptions
```haskell
isSyncException :: Exception e => e -> Bool
isSyncException e =
case fromException (toException e) of
Just (SomeAsyncException _) -> False
Nothing -> True
isAsyncException :: Exception e => e -> Bool
isAsyncException = not . isSyncException
```
Now to fix `throwIO` and `throwTo`. But first...
----
## Wrapper types
```haskell
data SyncExceptionWrapper =
forall e. Exception e => SyncExceptionWrapper e
instance Exception SyncExceptionWrapper
data AsyncExceptionWrapper =
forall e. Exception e => AsyncExceptionWrapper e
instance Exception AsyncExceptionWrapper where
toException = toException . SomeAsyncException
fromException se = do
SomeAsyncException e <- fromException se
cast e
```
----
## Converters
```haskell
toSyncException :: Exception e => e -> SomeException
toSyncException e =
case fromException se of
Just (SomeAsyncException _) ->
toException (SyncExceptionWrapper e)
Nothing -> se
where
se = toException e
```
```haskell
toAsyncException :: Exception e => e -> SomeException
toAsyncException e =
case fromException se of
Just (SomeAsyncException _) -> se
Nothing -> toException (AsyncExceptionWrapper e)
where
se = toException e
```
----
## Replacement throwers
```haskell
import qualified Control.Exception as EUnsafe
throwIO :: (MonadIO m, Exception e) => e -> m a
throwIO = liftIO . EUnsafe.throwIO . toSyncException
throwTo :: (Exception e, MonadIO m) => ThreadId -> e -> m ()
throwTo tid = liftIO . EUnsafe.throwTo tid . toAsyncException
impureThrow :: Exception e => e -> a
impureThrow = EUnsafe.throw . toSyncException
```
----
## Replacement catchers
Break up all "catching" functions into recovery and cleanup, e.g.:
* Recovery (rethrows immediately on async)
* `catch`
* `try`
* `handle`
* Cleanup (always rethrows after running cleanup)
* `bracket`
* `onException`
* `finally`
----
## Simplified catch
```haskell
import qualified Control.Exception as EUnsafe
catch :: Exception e => IO a -> (e -> IO a) -> IO a
catch f g = f `EUnsafe.catch` \e ->
if isSyncException e
then g e
-- intentionally rethrowing an async exception
-- synchronously, since we want to preserve
-- async behavior
else EUnsafe.throwIO e
```
Real version has some monad transformer nonsense involved
----
## Easy, safe async handling
* Stick to these modified helper functions
* You won't accidentally recover from an async exception
* Can even safely do Pokemon exception handling
```haskell
tryAny :: MonadUnliftIO m => m a -> m (Either SomeException a)
tryAny = try
main :: IO ()
main = tryAny (readFile "foo.txt") >>= print
```
We'll talk about libraries a bit later
---
## Avoid low level masking
* Low level masking is complicated
* Avoid it whenever possible
* Use higher level helper functions from helper modules
* But, sometimes, you'll need to know about this
* So we need to cover one more complication
----
## Uninterruptible masking
* `mask` prevents async exception polling happening _everywhere_
* However, it still allows "interruptible" actions to receive async exceptions
* Useful for avoiding deadlocks
```haskell
mask $ \restore -> do
a <- takeMVar m
restore (...) `catch` \e -> ...
```
* But sometimes this can _also_ prevent cleanup from running
----
## This is complicated
* Tradeoff between deadlock and guaranteed cleanup is tricky
* Github issue: https://github.com/fpco/safe-exceptions/issues/3
* Nifty trick: you can always "upgrade" `mask` to `uninterruptibleMask`
* Doesn't work from unmasked to masked
* If you have to use a masking function, think hard about which one you want
* Better yet: use an existing helper function
----
## Deadlock detection
What's the result of running this program?
```haskell
import Control.Concurrent
main :: IO ()
main = do
mvar <- newEmptyMVar
takeMVar mvar
```
Usually, it will be:
```
foo.hs: thread blocked indefinitely in an MVar operation
```
GHC runtime tries to detect `MVar` and `STM`-based deadlocks, but
won't always succeed (don't rely on it!)
----
## Uninterruptible deadlock
How about this?
```haskell
import Control.Concurrent
import Control.Exception
main :: IO ()
main = do
mvar <- newEmptyMVar
uninterruptibleMask_ $ takeMVar mvar
```
Actual deadlock occurs: async exception is blocked
----
## Interruptible deadlock
And normal mask?
```haskell
import Control.Concurrent
import Control.Exception
main :: IO ()
main = do
mvar <- newEmptyMVar
mask_ $ takeMVar mvar
```
* Async exception is received, since `takeMVar` is interruptible
* Good example of (un)interruptible
* And deadlock detection is handled via async exceptions... Right?
----
## Is it async?
How about this one?
```haskell
import Control.Concurrent
import UnliftIO.Exception
main :: IO ()
main = do
mvar <- newEmptyMVar :: IO (MVar ())
tryAny (takeMVar mvar) >>= print
putStrLn "Looks like I recovered!"
```
* `tryAny` catches all synchronous exceptions (based on type)
* Guess: will "Looks like I recovered!" be printed?
----
## Recovery allowed!
* `BlockedIndefinitely` exceptions are considered synchronous
* They're technically thrown from another thread
* However, they're generated by a local action
* Totally safe to recover from
* Confusing? Yes! Logical? Arguably yes!
---
## Helper library breakdown
Three libraries provide async exception safe APIs
* `enclosed-exceptions`: based on the older forking-based approach
(not covered here). I no longer recommend it
* `safe-exceptions`: types + the `exceptions` package
* `unliftio`: types + the `MonadUnliftIO` typeclass (my recommendation)
See also: https://www.youtube.com/watch?v=KZIN9f9rI34
---
## Rules for async safe handling
* If something _must_ happen, you must use a masking function
* If you catch an async exception, you must rethrow it ASAP
* No long cleanup code!
* You should minimize the amount of time spent in masked state
Using the right libraries will make this much simpler!
__ONTO THE EXAMPLES__
---
## Control flow
Who likes this code?
```haskell
import Control.Concurrent
import Control.Concurrent.Async
import Control.Monad
main :: IO ()
main = do
messages <- newChan
race_
(mapM_ (writeChan messages) [1..10 :: Int])
(forever $ do
readChan messages >>= print
-- simulate some I/O latency
threadDelay 100000)
```
Drops messages on the floor!
----
## Avoid async exceptions if you can
```haskell
import UnliftIO (concurrently_, atomically, finally)
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM.TBMQueue
import Data.Function (fix)
```
```haskell
main = do
messages <- newTBMQueueIO 5
concurrently_
(mapM_ (atomically . writeTBMQueue messages)
[1..10 :: Int]
`finally` atomically (closeTBMQueue messages))
(fix $ \loop -> do
mmsg <- atomically $ readTBMQueue messages
case mmsg of
Nothing -> return ()
Just msg -> do
print msg
-- simulate some I/O latency
threadDelay 100000 >> loop)
```
---
## Email challenge 1
Good or bad?
```haskell
bracket
openConnection closeConnection $ \conn ->
bracket
(sendHello conn)
(sendGoodbye conn)
(startConversation conn)
```
----
## Solution: bad!
* `bracket` for open and close: good
* `bracket` for sending goodbye: takes too long
* Network protocol demands it? Network protocol is broken!
* SIGKILL
* Machine dies
* Network disconnects
----
## Better code
```haskell
bracket
openConnection closeConnection $ \conn -> do
sendHello conn
res <- startConversation conn
sendGoodbye conn
return res
```
There are likely exceptions to this rule (no pun intended), but you
should justify each such exception very strongly.
---
## Email challenge 2
Good or bad `bracket`?
```haskell
bracket before after inner = mask $ \restore -> do
resource <- before
eresult <- try $ restore $ inner resource
after resource
case eresult of
Left e -> throwIO (e :: SomeException)
Right result -> return result
```
(Obviously, __don't write your own bracket__!)
----
## Solution: mostly good
* Masks exceptions around entire block: good!
* `before` is run with exceptions still masked: good!. `restore` would
break it
* `restore` inside of `try` around `inner`: good!
* Call `after` immediately after `inner`: good!
* Possible problem: should `after` have an `uninterruptibleMask`? [Arguably](https://github.com/fpco/safe-exceptions/issues/3)
* Rethrow the exception: good!
---
## Racing reads
What is the output of this program?
```haskell
import Control.Concurrent
import Control.Concurrent.Async
main :: IO ()
main = do
chan <- newChan
mapM_ (writeChan chan) [1..10 :: Int]
race (readChan chan) (readChan chan) >>= print
race (readChan chan) (readChan chan) >>= print
race (readChan chan) (readChan chan) >>= print
race (readChan chan) (readChan chan) >>= print
race (readChan chan) (readChan chan) >>= print
```
----
## Solution (on my machine)
```
Left 1
Left 3
Left 5
Left 7
Left 9
```
* Non-deterministic
* Left or Right could finish first
* Maybe the second read never started
* Could even get the even numbers
Maybe that seemd far-fetched...
----
## Timed read
This is logical. Is it valid?
```haskell
timeout 1000000 $ readChan chan
```
What if we simulate weird thread scheduling?
```haskell
mapM_ (writeChan chan) [1..10 :: Int]
mx <- timeout 1000000 $ do
x <- readChan chan
threadDelay 2000000
return x
print mx
readChan chan >>= print
```
----
## How to timeout?
Get inventive!
```haskell
tchan <- newTChanIO
atomically $ mapM_ (writeTChan tchan) [1..10 :: Int]
delayDone <- registerDelay 1000000
let stm1 = do
isDone <- readTVar delayDone
check isDone
return Nothing
stm2 = do
x <- readTChan tchan
unsafeIOToSTM $ threadDelay 2000000
return $ Just x
mx <- atomically $ stm1 <|> stm2
print mx
atomically (readTChan tchan) >>= print
```
---
## Forked threads
__Caveat__
* Use the `async` library wherever possible
* Prefer `concurrently`, `race`, etc
* Then use `Async`
* Only then use `forkIO` as a last resort
OK, that's out of the way...
----
## Cleanup in child
```haskell
main = do
putStrLn "Acquire in main thread"
tid <- forkIO $
(putStrLn "use in child thread" >> threadDelay maxBound)
`finally` putStrLn "cleanup in child thread"
killThread tid -- built on top of throwTo
putStrLn "Exiting the program"
```
Timing-dependent output:
```
Acquire in main thread
Exiting the program
```
Child doesn't call `finally` before it's killed!
----
## Unhelpful masking
```haskell
main = do
putStrLn "Acquire in main thread"
tid <- forkIO $ uninterruptibleMask_ $
(putStrLn "use in child thread" >> threadDelay maxBound)
`finally` putStrLn "cleanup in child thread"
killThread tid -- built on top of throwTo
putStrLn "Exiting the program"
```
Still didn't call `uninterruptibleMask_` before thread is killed!
----
## Mask before forking
Masking state is inherited, so:
```haskell
main = do
putStrLn "Acquire in main thread"
tid <- uninterruptibleMask_ $ forkIO $
(putStrLn "use in child thread" >> threadDelay maxBound)
`finally` putStrLn "cleanup in child thread"
killThread tid -- built on top of throwTo
putStrLn "Exiting the program"
```
What's the problem?
<div class="fragment">Deadlock! Cannot kill the child</div>
----
## Almost correct
Mask and restore
```haskell
main = do
hSetBuffering stdout LineBuffering
putStrLn "Acquire in main thread"
tid <- uninterruptibleMask $ \restore -> forkIO $
restore (putStrLn "use in child thread"
>> threadDelay maxBound)
`finally` putStrLn "cleanup in child thread"
killThread tid -- built on top of throwTo
putStrLn "Exiting the program"
```
The problem is "nuanced"
----
## The best solution
Use `forkIOWithUnmask`
```haskell
main = do
hSetBuffering stdout LineBuffering
putStrLn "Acquire in main thread"
tid <- uninterruptibleMask_ $ forkIOUnmask $ \unmask ->
unmask (putStrLn "use in child thread"
>> threadDelay maxBound)
`finally` putStrLn "cleanup in child thread"
killThread tid -- built on top of throwTo
putStrLn "Exiting the program"
```
Let's see why
----
## Inherited masking state (1)
```haskell
foo = mask $ \restore -> restore getMaskingState >>= print
bar = mask $ \restore -> do
forkIO $ restore getMaskingState >>= print
threadDelay 10000
baz = mask_ $ do
forkIOWithUnmask $ \unmask ->
unmask getMaskingState >>= print
threadDelay 10000
```
----
## Inherited masking state (2)
```haskell
main = do
putStrLn "foo"
foo
mask_ foo
uninterruptibleMask_ foo
putStrLn "\nbar"
bar
mask_ bar
uninterruptibleMask_ bar
putStrLn "\nbaz"
baz
mask_ baz
uninterruptibleMask_ baz
```
----
## Inherited masking state (3)
```
foo
Unmasked
MaskedInterruptible
MaskedUninterruptible
bar
Unmasked
MaskedInterruptible
MaskedUninterruptible
baz
Unmasked
Unmasked
Unmasked
```
In forked thread: want to be unmasked, _not_ the previous masking
state!
---
## forkIO and race
Remember me?
```haskell
badRace :: IO a -> IO b -> IO (Either a b)
badRace ioa iob = do
mvar <- newEmptyMVar
tida <- forkIO $ ioa >>= putMVar mvar . Left
tidb <- forkIO $ iob >>= putMVar mvar . Right
res <- takeMVar mvar
killThread tida
killThread tidb
return res
```
```haskell
main :: IO ()
main = badRace (return ()) (threadDelay maxBound) >>= print
```
```
Left ()
```
Good
----
## Masked?
```haskell
main :: IO ()
main = mask_
$ badRace (return ()) (threadDelay maxBound)
>>= print
```
Same thing: `Left ()`. But what about:
```haskell
main :: IO ()
main = uninterruptibleMask_
$ badRace (return ()) (threadDelay maxBound)
>>= print
```
Deadlock! `forkIO` inside `badRace` inherits masked state
----
## Fixing this bug
(There are other bugs, exercise for the reader)
```haskell
badRace :: IO a -> IO b -> IO (Either a b)
badRace ioa iob = do
mvar <- newEmptyMVar
tida <- forkIOWithUnmask $ \u -> u ioa
>>= putMVar mvar . Left
tidb <- forkIOWithUnmask $ \u -> u iob
>>= putMVar mvar . Right
res <- takeMVar mvar
killThread tida
killThread tidb
return res
```
---
## unsafePerformIO vs unsafeDupablePerformIO
* Couldn't get a good demonstration
* Great example of what goes wrong without async exceptions
* If you're curious, Trac ticket: https://ghc.haskell.org/trac/ghc/ticket/8502
* c/o Chris Allen, thanks!
---
## Links (1)
* General Haskell syllabus: https://www.fpcomplete.com/haskell-syllabus
* The `unliftio` library: https://www.stackage.org/package/unliftio
* Exception handling module: https://www.stackage.org/haddock/lts-11.1/unliftio-0.2.5.0/UnliftIO-Exception.html
* safe-exceptions documentation: https://haskell-lang.org/library/safe-exceptions
----
## Links (2)
* Exceptions best practices: https://www.fpcomplete.com/blog/2016/11/exceptions-best-practices-haskell
* Monad transformers talk
* Slides: https://www.snoyman.com/reveal/monad-transformer-state
* Video: https://www.youtube.com/watch?v=KZIN9f9rI34
---
## Questions? Comments?
* Thanks all!
* Let us know on Twitter what topics you're interested in next
* [@snoyberg](https://twitter.com/snoyberg/)
* [@FPComplete](https://twitter.com/fpcomplete)
* For Partnering opportunities please email: [[email protected]](mailto:[email protected])
* Slides: https://www.snoyman.com/reveal/async-exception-handling
* Blog post on its way!