Pergunta

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.

Foi útil?

Solução

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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top