Timing out computations

From HaskellWiki
Jump to navigation Jump to search

A problem that frequently arises is that of constructing an IO action which terminates after a given period of time, regardless of whether it has finished the computation it was intended to perform. There are a variety of ways in which this can be accomplished.

One implementation for various efficient timeout combinators can be found in HAppS.Util.TimeOut (GPL licenced).

One way is to consider the more general problem of executing a number of competing actions in parallel, and returning the result of the first to be finished its task, an interesting problem in and of itself.

What we do is to create an initially empty MVar, spawn threads for each of the competing computations, and have them all compete to execute their action and then put the result into the MVar. In the main thread we try to take from the MVar, which blocks until one of the threads completes its task. We then kill all of the threads, and return the winning result.

import Control.Concurrent

compete :: [IO a] -> IO a
compete actions = do
    mvar <- newEmptyMVar
    tids <- mapM (\action -> forkIO $ action >>= putMVar mvar) actions
    result <- takeMVar mvar
    mapM_ killThread tids
    return result

In order to implement the timeout, we just have two processes compete: the one to attempt, with its result wrapped in the Just constructor, and one which waits the specified time (in microseconds) and then returns Nothing.

timeout :: Int -> IO a -> IO (Maybe a)
timeout usec action = compete [fmap Just action, threadDelay usec >> return Nothing]

A related problem is that of iterating a pure function for as many steps as possible until a given time limit passes, and returning the last computed result afterward. One can achieve this again using threads as follows:

import Control.Concurrent
import Control.Exception

timeoutIterate msec f x = do
    mvar <- newMVar x
    let loop = do
           x <- takeMVar mvar
           evaluate (f x) >>= putMVar mvar
    thread <- forkIO loop
    threadDelay msec
    u <- takeMVar mvar
    killThread thread
    return u

Without threads, using getClockTime to check if enough time has passed, it looks like this:

import Control.Exception
import System.Time

getClockTimeMS = do
    (TOD s p) <- getClockTime
    return $ fromIntegral (s * 1000 + p `div` 10^6)

timeoutIterate' msec f x = do
    t <- getClockTimeMS
    y <- evaluate (f x)
    t' <- getClockTimeMS
    timeoutIterate (msec - (t' - t)) f y

Note that in both cases, the use of evaluate is important to ensure that all of the evaluation actually occurs in the given timeframe and not lazily afterward.

(The above example doesn't seem likely to work, how about this instead:)

-- microseconds
getClockTimeMS = do
    (TOD s p) <- getClockTime
    return $ fromIntegral (s * 1000000 + p `div` 10^6)
timeoutIterate msec f x = do
  t <- getClockTimeMS
  timeoutIterate' (t + msec) f x

timeoutIterate' fin f x = do
  t <- getClockTimeMS
  if t > fin then
      return x
      do y <- evaluate (f x)
         t' <- getClockTimeMS
         timeoutIterate' fin f y