Thanks very much for these ideas.  Peter Verswyvelen suggested running the
example repeatedly to see if it always runs correctly.  He found, and I
verified, that the example runs fine with Bertram's last version of unamb
below, *unless* it's compiled with -threaded and run with +RTS -N2.  In the
latter case, it locks up after a while.

I also tried a version with brackAsync and found that it eventually locks up
even under ghci.  When compiled & run multi-threaded, it locks up almost
immediately.

I've attached a module, TestRace.hs, containing these experiments.

    - Conal

On Sat, Dec 27, 2008 at 6:03 PM, Bertram Felgenhauer <
bertram.felgenha...@googlemail.com> wrote:

> Sterling Clover wrote:
> > On Dec 27, 2008, at 9:02 AM, Bertram Felgenhauer wrote:
> >> In the above code, there is a small window between catching the
> >> ThreadKilled exception and throwing it again though, where other
> >> exceptions may creep in. The only way I see of fixing that is to use
> >> 'block' and 'unblock' directly.
> >
> > That certainly seems to do the trick for the simple example at least. One
> > way to reason about it better would be, instead of folding everything
> into
> > the race function, to simply modify ghc's bracket function to give us the
> > behavior we'd prefer (speaking of which, I recall there's something in
> the
> > works for 6.12 or so to improve rethrowing of asynchronous exceptions?)
> >
> > brackAsync before after thing =
> >   block (do
> >     a <- before
> >     r <- catch
> >            (unblock (thing a))
> >            (\_ -> after a >> myThreadId >>= killThread >>
> >                   brackAsync before after thing )
> >     after a
> >     return r
> >  )
> >     where threadKilled ThreadKilled = Just ()
> >           threadKilled _            = Nothing
>
> This code turns any exception into ThreadKilled further down the stack.
>
>  (\e -> do
>       after a
>       myThreadId >>= flip throwTo (e :: SomeException)
>       ...
>
> might do the trick.
>
> My assumption was that anything but 'ThreadKilled' would be a
> real error. This isn't really true, I guess - thanks to throwTo,
> any exception could be asynchronous.
>
> If an exception is thrown, 'after a' is run again after the computation
> has resumed.
>
> That's why I did the cleanup within the 'catch'.
>
> But there's no reason why you couldn't do that as well:
>
>  brackAsync before after thing =
>    block $ do
>      a <- before
>       catch  (unblock (thing a) >>= \r -> after a >> return r) $
>             \e -> do
>                    after a
>                    myThreadId >>= flip throwTo (e :: SomeException)
>                    brackAsync before after thing )
>
> > This brackAsync just drops in to the previous code where bracket was and
> > appears to perform correctly.
>
> Right. 'race' should also unblock exceptions in the worker threads,
>
>    withThread u v = brackAsync (forkIO (unblock u)) killThread (const v)
>
> but that's an independent change.
>
> > Further, if we place a trace after the
> > killThread, we se it gets executed once when the example is read (i.e. a
> > resumption) but it does not get executed if the (`seq` v) is removed from
> > the example So this gives me some hope that this is actually doing what
> > we'd like. I don't doubt it may have further kinks however.
>
> At least the GHC RTS has support for the hard part - unwinding the stack
> so that computations can be resumed seamlessly.
>
> I'm not sure which of the approaches I like better - it seems that we
> have a choice between turning async exceptions into sync ones or vice
> versa, and neither choice is strictly superior to the other.
>
> Enjoy,
>
> Bertram
>
> 'race' update:
> - Bugfix: Previously, only AsyncException-s would be caught.
>  Use 'fromException' to select the ThreadKilled exception.
> - I tried using a custom 'SuspendException' type, but this resulted in
>  'test: SuspendException' messages on the console, while ThreadKilled
>  is silently ignored... as documented:
>
> http://www.haskell.org/ghc/docs/latest/html/libraries/base/Control-Concurrent.html#v%3AforkIO
>     (http://tinyurl.com/9t5pxs)
> - Tweak: Block exceptions while running 'cleanup' to avoid killing
>  threads twice.
> - Trick: takeMVar is a blocking operation, so exceptions can be
>  delivered while it's waiting - there's no need to use 'unblock' for
>  this. In other words,  unblock (takeMVar v)  and  takeMVar v  are
>  essentially equivalent for our purposes.
>
> race :: IO a -> IO a -> IO a
> race a b = block $ do
>    v <- newEmptyMVar
>     let t x = unblock (x >>= putMVar v)
>     ta <- forkIO (t a)
>    tb <- forkIO (t b)
>    let cleanup = killThread ta >> killThread tb
>     (do r <- takeMVar v; cleanup; return r) `catch`
>         \e -> cleanup >>
>            case fromException e of
>                Just ThreadKilled -> do
>                     myThreadId >>= killThread
>                    unblock (race a b)
>                 _ -> throwIO e
> _______________________________________________
> Reactive mailing list
> react...@haskell.org
> http://www.haskell.org/mailman/listinfo/reactive
>
{-# OPTIONS_GHC -Wall #-}

-- TestRace.hs.  Compile & run:
--   ghc --make -threaded TestRace.hs
--   ./TestRace +RTS -N2

-- Prints consecutive natural numbers for a while, and then gets stuck.
-- Seems to run fine without -threaded/N2

import Prelude hiding (catch)
import System.IO.Unsafe
import Control.Monad.Instances () -- for function functor
import Control.Concurrent
import Control.Exception

test :: Int -> Int
test x = f (f x) where f v = (x `unamb` v) `seq` v

main :: IO ()
main = mapM_ (print . test) [0..]


unamb :: a -> a -> a
a `unamb` b = unsafePerformIO (evaluate a `race` evaluate b)

-- | Race two actions against each other in separate threads, and pick
-- whichever finishes first.  See also 'amb'.
race :: IO a -> IO a -> IO a

-- -- Old version.  Doesn't take care to recursively terminate descendent
-- -- threads.  This version seems to work forever, but with degraded
-- -- performance.  I guess there are more & more useless live threads.
-- a `race` b = do v  <- newEmptyMVar
--                 let f x = forkIO (putCatch x v)
--                 ta <- f a
--                 tb <- f b
--                 x  <- takeMVar  v
--                 killThread ta
--                 killThread tb
--                 return x


-- -- New version, based on suggestions from Sterling Clover and Bertram
-- -- Felgenhauer.  This one seems to work correctly and perform well, unless
-- -- compiled with -threaded and run with +RTS -N2, which leads to lock-up
-- -- after a while.
-- race a b = block $ do
--    v <- newEmptyMVar
--    let f x = forkIO (unblock (putCatch x v))
--    ta <- f a
--    tb <- f b
--    let cleanup = killThread ta >> killThread tb
--    (do r <- takeMVar v; cleanup; return r) `catch`
--        \e -> do cleanup
--                 case fromException e of
--                     Just ThreadKilled ->
--                       -- kill self synchronously and then retry if
--                       -- evaluated again.
--                       do myThreadId >>= killThread
--                          unblock (race a b)
--                     _ -> throwIO e


-- This one locks up after a while even in ghci.  When compiled -threaded
-- and run +RTS -N2, it locks up almost immediately.
a `race` b = do
   v <- newEmptyMVar
   let t x = x >>= putMVar v
   withThread (t a) $ withThread (t b) $ takeMVar v
 where
  withThread u v = brackAsync (forkIO u) killThread (const v)

brackAsync :: IO t -> (t -> IO b) -> (t -> IO a) -> IO a
brackAsync before after thing =
  block $ do
    a <- before
    catch  (unblock (thing a) >>= \r -> after a >> return r) $
           \e -> do
                  after a
                  myThreadId >>= flip throwTo (e :: SomeException)
                  brackAsync before after thing




putCatch :: IO a -> MVar a -> IO ()
putCatch act v = (act >>= putMVar v) `catches` 
  [ Handler $ \ (ErrorCall _)     -> return ()
  , Handler $ \ BlockedOnDeadMVar -> return ()
  -- This next handler hides bogus black holes, which show up as
  -- "<<loop>>" messages.  I'd rather eliminate the problem than hide it.
  -- , Handler $ \ NonTermination    -> return ()
  ]
_______________________________________________
Glasgow-haskell-users mailing list
Glasgow-haskell-users@haskell.org
http://www.haskell.org/mailman/listinfo/glasgow-haskell-users

Reply via email to