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
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
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.