I created this small program that creates a long-running thunk that eventually fails with an exception. Then, multiple threads try to evaluate it.
import Control.Monad
import Control.Concurrent
import Control.Concurrent.MVar
main = do
let thunk = let p = product [1..10^4]
in if p `mod` 2 == 0 then error "exception"
else ()
children <- replicateM 2000 (myForkIO (print thunk))
mapM_ takeMVar children
-- | Spawn a thread and return a MVar which can be used to wait for it.
myForkIO :: IO () -> IO (MVar ())
myForkIO io = do
mvar <- newEmptyMVar
forkFinally io (\_ -> putMVar mvar ())
return mvar
Increasing the number of threads has clearly no impact on the computation, which suggests that a failed thunk keeps the exception as the result. Is it true? Is this behavior documented/specified somewhere?
Update: Changing the forkFinally
line to
forkFinally io (\e -> print e >> putMVar mvar ())
confirms that each thread fails with the exception.
Let me answer this question by showing how GHC actually does this, using the ghc-heap-view
library. You can probably reproduce this with ghc-vis
and get nice pictures.
I start by creating a data structure with an exception value somewhere:
Prelude> :script /home/jojo/.cabal/share/ghc-heap-view-0.5.1/ghci
Prelude> let x = map ((1::Int) `div`) [1,0]
At first it is purely a thunk (that seems to involve various type classes):
Prelude> :printHeap x
let f1 = _fun
in (_bco [] (_bco (D:Integral (D:Real (D:Num _fun _fun _fun _fun _fun _fun _fun) (D:Ord (D:Eq _fun _fun) _fun _fun _fun _fun _fun _fun _fun) _fun) (D:Enum _fun _fun f1 f1 _fun _fun _fun _fun) _fun _fun _fun _fun _fun _fun _fun) _fun) _fun)()
Now I evaluate the non-exception-throwing-parts:
Prelude> (head x, length x)
(1,2)
Prelude> System.Mem.performGC
Prelude> :printHeap x
[I# 1,_thunk (_fun (I# 1)) (I# 0)]
The second element of the list is still just a “normal” thunk. Now I evaluate this, get an exception, and look at it again:
Prelude> last x
*** Exception: divide by zero
Prelude> System.Mem.performGC
Prelude> :printHeap x
[I# 1,_thunk (SomeException (D:Exception _fun (D:Show _fun _fun _fun) _fun _fun) DivideByZero())]
You can see it is now a thunk that references an SomeException
object. The SomeException
data constructor has type forall e . Exception e => e -> SomeException
, so the second parameter of the constructor is the DivideByZero
constructor of the ArithException
exception, and the first parameter the corresponding Exception
type class instance.
This thunk can be passed around just like any other Haskell value and will, if evaluated, raise the exception again. And, just like any other value, the exception can be shared:
Prelude> let y = (last x, last x)
Prelude> y
(*** Exception: divide by zero
Prelude> snd y
*** Exception: divide by zero
Prelude> System.Mem.performGC
Prelude> :printHeap y
let x1 = SomeException (D:Exception _fun (D:Show _fun _fun _fun) _fun _fun) DivideByZero()
in (_thunk x1,_thunk x1)
The same things happen with threads and MVars, nothing special there.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With