The darker corners of throwTo

Posted on June 11, 2013

We will assume

import System.IO.Unsafe (unsafePerformIO, unsafeInterleaveIO)
import System.IO.Error (catchIOError)
import Control.Concurrent (throwTo, myThreadId, threadDelay)
import Control.Concurrent.Async (async, cancel, wait)
import qualified Control.Exception as Ex

in the remainder of this post.

Whatever work the target thread was doing when the exception was raised is not lost: the computation is suspended until required by another thread.

We can observe this directly when using unsafePerformIO :

fib :: Int -> Int
fib 0 = 1
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

fooIO :: IO Int
fooIO = go 0 37
  where
    go !acc 30 = return acc
    go !acc n  = print n >> go (acc + fib n) (n - 1)

test :: Int -> IO ()
test foo = do
  a1 <- async $ print foo
  threadDelay 500000
  a2 <- async $ print foo
  threadDelay 4000000
  putStrLn "Killing a1"
  cancel a1
  wait a2

main :: IO ()
main = test $ unsafePerformIO fooIO

The test starts by spawning a new thread that begins summing the 37th down to the 31st Fibonacci number by demanding the value of the foo = unsafePerformIO fooIO thunk. After half a second a second thread is spawned which blocks on the same thunk, and will wait for the first thread to finish. Consider, however, what happens when we then kill the first thread (the cancel function from async throws an asynchronous ThreadKilled exception to the destination thread): the second thread seamlessly takes over the computation of the thunk. Compiling the above example (without any ghc options) and running it gives

37
36
Killing a1
35
34
33
32
31
98809577

Nice. But now consider what happens when we replace fooIO by

fooIO_1 :: IO Int
fooIO_1 = fooIO `catchIOError` \_ -> return 0

Since no IO exception is ever thrown, you would think that this should make no difference right? Wrong:

37
36
Killing a1
ExTest: thread killed

What happened? catchIOError actually catches all exceptions, not just IOErrors; however, it then re-throws all other exceptions. fooIO_1 is actually equivalent to

fooIO_2 :: IO Int
fooIO_2 = fooIO `Ex.catch` \e ->
  case Ex.fromException (e :: Ex.SomeException) of
    Just (ioerror :: IOError) -> return 0
    Nothing                   -> Ex.throwIO e

The asynchronous exception thrown by cancel is caught and then rethrown as a synchronous exception. This synchronous exception is not caught, and becomes the final value of the foo thunk. Hence, when the second thread takes over the evaluation of this thunk, it finds the exception and gets killed.

So what if we want to catch IO exceptions, and deal with them, but still be able to rethrow asynchronous exceptions? Is there a way? Well, yes, we can rethrow the asynchronous exception again as an asynchronous exception. From the documentation ofthrowTo again:

If the target of throwTo is the calling thread, then the behaviour is the same as throwIO, except that the exception is thrown as an asynchronous exception. This means that if there is an enclosing pure computation, which would be the case if the current IO operation is inside unsafePerformIO or unsafeInterleaveIO, that computation is not permanently replaced by the exception, but is suspended as if it had received an asynchronous exception.

But then we have to specify precisely how to continue after the rethrow:

fooIO_3 :: IO Int
fooIO_3 = fooIO `Ex.catch` \e ->
  case Ex.fromException (e :: Ex.SomeException) of
    Just (ioerror :: IOError) -> return 0
    Nothing                   -> do tid <- myThreadId
                                    throwTo tid e
                                    fooIO_3

However, this code is not equivalent to the version without any catches at all, because we now explicitly restart the computation after the asynchronous exception:

# ./ExTest 
37
36
Killing a1
37
36
35
34
33
32
31
98809577

(Note that the computation starts back at 37 after “Killing a1”.) I don’t think that there is a way around it (but if there is, please let me know :) ) The other problem with fooIO_3 is that it is rethrowing all exceptions asynchronously; unfortunately, there is no way to reliable detect if an an exception was thrown synchronously or asynchronously, sincethrowTo can be used to throw any kind of exception (see ghc bug 5092). At the moment, probably your best bet is to rethrow AsyncExceptions asynchronously and the rest synchronously, but that’s a “best effort” only and may break. If you are using HEAD, you can do slightly better, because there is an ‘asynchronous exception hierarchy’, based on aSomeAsyncException data type analogous toSomeException ; however, that still does not give you the ability to check if an arbitrary check was thrown synchronously or asynchronously.

Note that precisely the same problems arise with unsafeInterleaveIO . Conclusion: don’t use System.IO.Unsafe – but you already knew that.

Further reading: a number of ghc bugs are related to this problem; see the discussion at 2558, 3997 and 5092 ; an old email thread on haskell-cafe, and the documentation of Control.Exception.