Smarter termination for thread racing
I realized in the shower this morning that there’s a serious flaw in my unamb implementation as described in Functional concurrency with unambiguous choice. Here’s the code for racing two computations:
race :: IO a -> IO a -> IO a
a `race` b = do v <- newEmptyMVar
ta <- forkPut a v
tb <- forkPut b v
x <- takeMVar v
killThread ta
killThread tb
return x
forkPut :: IO a -> MVar a -> IO ThreadId
forkPut act v = forkIO ((act >>= putMVar v) `catch` uhandler `catch` bhandler)
where
uhandler (ErrorCall "Prelude.undefined") = return ()
uhandler err = throw err
bhandler BlockedOnDeadMVar = return ()
The problem is that each of the threads ta
and tb
may have spawned other threads, directly or indirectly.
When I kill them, they don’t get a chance to kill their sub-threads.
If the parent thread does get killed, it will most likely happen during the takeMVar
.
My first thought was to use some form of garbage collection of threads, perhaps akin to Henry Baker’s paper The Incremental Garbage Collection of Processes. As with memory GC, dropping one consumer would sometimes result is cascading de-allocations. That cascade is missing from my implementation above.
Or maybe there’s a simple and dependable manual solution, enhancing the method above.
I posted a note asking for ideas, and got the following suggestion from Peter Verswyvelen:
I thought that killing a thread was basically done by throwing a ThreadKilled exception using throwTo. Can’t these exception be caught?
In C#/F# I usually use a similar technique: catch the exception that kills the thread, and perform cleanup.
Playing with Peter’s suggestion works out very nicely, as described in this post.
There is a function that takes a clean-up action to be executed even if the main computation is killed:
finally :: IO a -> IO b -> IO a
Using this function, the race
definition becomes a little shorter and more descriptive:
a `race` b = do v <- newEmptyMVar
ta <- forkPut a v
tb <- forkPut b v
takeMVar v `finally`
(killThread ta >> killThread tb)
This code is vulnerable to being killed after the first forkPut and before the second one, which would then leave the first thread running. The following variation is a bit safer:
a `race` b = do v <- newEmptyMVar
ta <- forkPut a v
(do tb <- forkPut b v
takeMVar v `finally` killThread tb)
`finally` killThread ta
Though I guess it’s still possible for the thread to get killed after the first fork and before the next statement begins. Also, this code difficult to write and read. The general pattern here is to fork a thread, do something else, and kill the thread. Give that pattern a name:
forking :: IO () -> IO b -> IO b
forking act k = do tid <- forkIO act
k `finally` killThread tid
The post-fork action in both cases is to execute another action (a
or b
) and put the result into the mvar v
.
Removing the forkIO
from forkPut
, leaves putCatch
:
putCatch :: IO a -> MVar a -> IO ()
putCatch act v = (act >>= putMVar v) `catch` uhandler `catch` bhandler
where
uhandler (ErrorCall "Prelude.undefined") = return ()
uhandler err = throw err
bhandler BlockedOnDeadMVar = return ()
Combe forking
and putCatch
for convenience:
forkingPut :: IO a -> MVar a -> IO b -> IO b
forkingPut act v k = forking (putCatch act v) k
Or, in the style of Semantic editor combinators,
forkingPut = (result.result) forking putCatch
Now the code is tidy and safe:
a `race` b = do v <- newEmptyMVar
forkingPut a v $
forkingPut b v $
takeMVar v
Recall that there’s a very slim chance of the parent thread getting killed after spinning a child and before getting ready to kill the sub-thread (i.e., the finally
).
If this case happens, we will not get an incorrect result.
Instead, an unnecessary thread will continue to run and write its result into an mvar that no one is reading.
Luke Palmer:
So the result is correct, but we still have a leaked thread. This could lead to unpredictable memory leaks: consider the case where the computation is nonterminating and allocating.
You can use Control.Exception.block to ensure safety:
19 December 2008, 1:51 amSpencer Janssen:
Control.Exception.bracket will avoid the race condition, ensuring that child threads are always killed:
19 December 2008, 2:03 amChung-chieh Shan:
Very nice — “tidy and safe” indeed!
19 December 2008, 7:21 amPeter Verswyvelen:
Hi Conal,
Nice to see it works. Simon Marlow also replied on your question. He posted code for the timeout function, which does something similar. See http://darcs.haskell.org/packages/base/System/Timeout.hs
At home I played a bit with the bracket function and tested the following code:
Now, under Windows, when I run this with GHCI, I get 1231abcabc, as expected.
But when compiling with ghc, and running the EXE, I get 1231abacb.
I’m missing a ‘c’, which might mean some tb thread is not killed…
When compiling with GHC -threaded and running the EXE (with or without +RTS -N2), I get 1231abacbc, so correct again.
Bob has similar problems under OSX, he just got abc…
What did I do wrong? Might this indicate a bug in GHC’s runtime?
19 December 2008, 9:20 amPeter Verswyvelen:
Mmm, I used Spencer’s version, and added some more detail, but I still get the problem (it now does work with -threaded and +RTS -N2…). So the interpreted version kills all threads, the compiled version only does it with +RTS -N2, or at least the last putStrLn does not seem to be called.
Ugly code follows:
Output with GHCI:
Output from compiled EXE:
So “Killed ThreadId 7″ is not printed
Output with +RTS -N2:
19 December 2008, 9:38 amconal:
Peter,
I think the problem here is simply that the process exits before the threads all get a chance to terminate. In my experiments, adding even a
19 December 2008, 9:51 pmyield
to the end ofmain
seems to ensure that all threads get killed.Peter Verswyvelen:
Indeed. But I would like to see code that does not allow the process to exit before all of these thread finalizers did their cleanup code (since finalizers may run critical code, like turning off a nuclear power station to prevent a meltdown ;). Okay, not really an issue for Reactive.
21 December 2008, 11:12 amconal:
I’m with you, Peter. I’d like a simple way to guarantee running the cleanup code.
21 December 2008, 2:22 pmconal:
I’ve been experimenting with various
race
implementations, including the ones given above. All of these smarter versions break when given the following example:The error message (in ghci) is “*** Exception: thread killed”.
Definitions that give this error include my three
and Luke’s:
and Spencer’s:
My original version gives the answer I want, namely 1:
Here’s a definition that comes from inlining
finally
in the first smart version above:It also fails. However, if I remove the inner
unblock
, I get 1, as desired:What is going on here? What is causing “*** Exception: thread killed”?
Here is a possibly-relevant excerpt from the documentation:
23 December 2008, 10:32 pmPeter Verswyvelen:
Well, I tried the following version:
Using GHCi or GHC –make, I get the thread killed exception…
When using GHC –make -O, I don’t get the thread killed exception, just get the correct answer 1
Sometimes, using GHCi, it just blocks.
I get the same with Spencer’s version… Haven’t tried the other versions, but I expect they will also behave correctly with GHC -O…
24 December 2008, 4:09 pmPeter Verswyvelen:
Not sure if this helps, but if I replace
by
I get back 1.
But of course that might not be what you want, since the semantics of evaluate are different (I don’t fully understand from the GHC documentation what evaluate really does…)
If I replace amb by “race
on
(($!) return)” I get the exception againI also made a version that hangs under GHCi instead of throwing the exception:
The version above – when run via GHCi – blocks. If I press CTRL-C it interrupts. When running again in the same session, I get 1. When running a compiled version without optimization, I get the exception. When running a compile version with optimization, I get 1.
Weird behavior…
25 December 2008, 6:09 amConal Elliott » Blog Archive » Another angle on functional future values:
[…] About « Smarter termination for thread racing […]
6 January 2009, 2:45 pm