[Haskell-cafe] Faster timeout but is it correct?
Bas van Dijk
v.dijk.bas at gmail.com
Tue Feb 22 23:50:40 CET 2011
On 22 February 2011 19:59, Bertram Felgenhauer
<bertram.felgenhauer at googlemail.com> wrote:
> Bas van Dijk wrote:
>> On 19 February 2011 00:04, Bas van Dijk <v.dijk.bas at gmail.com> wrote:
>> > So, since the new implementation is not really faster in a
>> > representative benchmark and above all is buggy, I'm planning to ditch
>> > it in favour of the event-manager based timeout.
>>
>> The patch is ready for review:
>>
>> http://hackage.haskell.org/trac/ghc/attachment/ticket/4963/faster_timeout.dpatch
>
> (For reference, this is the proposed timeout code:)
>
> | timeout :: Int -> IO a -> IO (Maybe a)
> | timeout usecs f
> | | usecs < 0 = fmap Just f
> | | usecs == 0 = return Nothing
> | | otherwise = do
> | myTid <- myThreadId
> | Just mgr <- readIORef eventManager
> | mask $ \restore -> do
> | key <- registerTimeout mgr usecs $ \key ->
> | throwTo myTid $ Timeout key
> | let unregTimeout = M.unregisterTimeout mgr key
> | (restore (fmap Just f) >>= \mb -> unregTimeout >> return mb)
> | `catch` \e ->
> | case fromException e of
> | Just (Timeout key') | key' == key -> return Nothing
> | _ -> unregTimeout >> throwIO e
>
> What happens if the timeout triggers while the exception handler is
> running? I.e., we have the following sequence of events:
>
> 1. registerTimeout
> 2. (fmap Just f) raises an exception, or the thread gets killed otherwise.
> 3. We enter the `catch` handler, with the corresponding exception.
> 4. The timeout expires, and the event Manager runs the IO action, i.e.
> throwTo myTid $ Timeout key
> 5. And now we have a pending Timeout exception which escapes the 'timeout'.
> The unregTimeout will come too late.
Bummer! You're right.
But maybe we can catch and ignore a potential pending Timeout
exception: (code not tested and profiled yet)
timeout :: Int -> IO a -> IO (Maybe a)
timeout usecs f
| usecs < 0 = fmap Just f
| usecs == 0 = return Nothing
| otherwise = do
myTid <- myThreadId
Just mgr <- readIORef eventManager
mask $ \restore -> do
key <- registerTimeout mgr usecs $ \key ->
throwTo myTid $ Timeout key
let unregTimeout = M.unregisterTimeout mgr key
(restore (fmap Just f) >>= \mb -> unregTimeout >> return mb)
`catch` \e ->
case fromException e of
Just (Timeout key') | key' == key -> return Nothing
_ -> do (unregTimeout >> allowInterrupt)
`catch` \(Timeout _) -> return ()
throwIO e
Note I use the newly proposed[1] allowInterrupt:
-- | When invoked inside 'mask', this function allows a blocked
-- asynchronous exception to be raised, if one exists. It is
-- equivalent to performing an interruptible operation (see
-- #interruptible#), but does not involve any actual blocking.
--
-- When called outside 'mask', or inside 'uninterruptibleMask', this
-- function has no effect.
allowInterrupt :: IO ()
allowInterrupt = unsafeUnmask $ return ()
> I've stumbled on another problem with the timeout function. Is this
> already known? Namely, the current implementation has trouble protecting
> against asynchronous exceptions, which can cause Timeout exceptions to
> escape from the corresponding 'timeout' call. The following program
> demonstrates this issue. (tested on ghc 7.0.1 using the threaded runtime)
>
> {-# LANGUAGE ScopedTypeVariables #-}
> import System.Timeout
> import Control.Exception
> import Control.Concurrent
> import Control.Monad
> import Prelude hiding (catch)
>
> delay = threadDelay 1000
>
> test = do
> let act = timeout 1 (threadDelay 1) >> delay
> act' = (act `catch` \ThreadKilled -> return ()) >> delay
> tid <- forkIO $
> act' `catch` \(e :: SomeException) ->
> putStr $ "gotcha: " ++ show e ++ "!\n"
> forkIO $ (threadDelay 10) >> killThread tid
> return ()
>
> main = do
> replicateM_ 1000 test
> threadDelay 100000
>
> (Will print gotcha: <<timeout>>! for every escaping Timout exception.)
> What I believe happens is that the 'killThread' in the timeout function
> is interrupted by the 'killThread' from the test program; as a result,
> the forked timeout thread continues to run after the timeout function
> itself has finished.
>
> Protecting against this seems hard, if not impossible. Even if we
> introduce a lock
> lock <- newMVar ()
> and let the timeout thread take the lock before throwing the exception
> ...
> forkIO (threadDelay n >> takeMVar lock >> throwTo pid ex)
> ...
> when handling the exception we still face a problem: We can use
> tryTakeMVar lock to stop the timeout thread from killing us, and to
> detect whether it's already too late for that. However, in that latter
> case, we will have to wait for the Timeout exception to arrive, in
> order to filter it; that means we will have to catch and remember all
> other pending async exception first, filter the Timeout exception, and
> then re-raise all the exceptions again.
>
> I suspect that the event manager based implementation will face the
> same problem.
Actually the event manager based implementation totally crashes on
your example, so again: bummer! I get the following error:
"gotcha: user error (Pattern match failure in do expression at
libraries/base/System/Event/Thread.hs:208:9-16)!"
Line 208:
Just mgr <- readIORef eventManager
I assumed that pattern match was safe because it's also used like that
in other places in the event manager (threadDelay, registerDelay,
closeFdWith and threadWait). I guess I was wrong...
All in all I have to seriously study this some more.
Thanks,
Bas
[1] http://hackage.haskell.org/trac/ghc/ticket/4857
More information about the Libraries
mailing list