简体   繁体   English

如何在GHCJS程序中定期执行操作?

[英]How to execute an action periodically in a GHCJS program?

应该通过Javascript使用setInterval ,还是使用一些基于线程的惯用解决方案?

Using setInterval posed some challenges and comments from Alexander, Erik and Luite himself led me to try threads. 使用setInterval提出亚历山大,Erik和Luite自己的一些挑战和评论让我尝试了线程。 This worked seamlessly, with very clean code similar to the following: 这无缝地工作,代码非常干净,类似于以下内容:

import Control.Concurrent( forkIO, threadDelay )
import Control.Monad( forever )

... within an IO block
threadId <- forkIO $ forever $ do
  threadDelay (60 * 1000 * 1000) -- one minute in microseconds, not milliseconds like in Javascript!
  doWhateverYouLikeHere

Haskell has the concept of lightweight threads so this is the idiomatic Haskell way to run an action in an asynchronous way as you would do with a Javascript setInterval or setTimeout . Haskell具有轻量级线程的概念,因此这是以异步方式运行动作的惯用Haskell方式,就像使用Javascript setIntervalsetTimeout

If you don't care about the motivation, just scroll to my best solution runPeriodicallyConstantDrift below. 如果你不关心动机,只需滚动到我的最佳解决方案runPeriodicallyConstantDrift下面。 If you prefer a simpler solution with worse results, then see runPeriodicallySmallDrift . 如果您更喜欢结果较差的简单解决方案,请参阅runPeriodicallySmallDrift

My answer is not GHCJS specific, and has not been tested on GHCJS, only GHC, but it illustrates problems with the OP's naive solution . 我的回答不是针对GHCJS的,并且没有经过GHCJS测试,只有GHC,但它说明了OP天真解决方案的问题

First Strawman Solution: runPeriodicallyBigDrift 第一个runPeriodicallyBigDrift解决方案: runPeriodicallyBigDrift

Here's my version of the OP's solution, for comparison below: 这是我的OP解决方案的版本,用于下面的比较:

import           Control.Concurrent ( threadDelay )
import           Control.Monad ( forever )

-- | Run @action@ every @period@ seconds.
runPeriodicallyBigDrift :: Double -> IO () -> IO ()
runPeriodicallyBigDrift period action = forever $ do
  action
  threadDelay (round $ period * 10 ** 6)

Assuming "execute an action periodically" means the action starts every period many seconds, the OP's solution is problematic because the threadDelay doesn't take into account the time the action itself takes. 假设“定期执行一个动作”意味着动作每隔几个周期开始一次, OP的解决方案threadDelay问题,因为threadDelay没有考虑动作本身所花费的时间。 After n iterations, the start time of the action will have drifted by at least the time it takes to run the action n times! 在n次迭代之后,动作的开始时间将至少漂移到运行动作n次所需的时间!

Second Strawman Solution: runPeriodicallySmallDrift 第二个runPeriodicallySmallDrift解决方案: runPeriodicallySmallDrift

So, we if we actually want to start a new action every period, we need to take into account the time it takes the action to run. 因此,如果我们实际上想要在每个时段开始新的操作,我们需要考虑操作所需的时间。 If the period is relatively large compared to the time it takes to spawn a thread, then this simple solution may work for you: 如果周期与生成线程所需的时间相比相对较大,那么这个简单的解决方案可能对您有用:

import           Control.Concurrent ( threadDelay )
import           Control.Concurrent.Async ( async, link )
import           Control.Monad ( forever )

-- | Run @action@ every @period@ seconds.
runPeriodicallySmallDrift :: Double -> IO () -> IO ()
runPeriodicallySmallDrift period action = forever $ do
  -- We reraise any errors raised by the action, but
  -- we don't check that the action actually finished within one
  -- period. If the action takes longer than one period, then
  -- multiple actions will run concurrently.
  link =<< async action
  threadDelay (round $ period * 10 ** 6)

In my experiments (more details below), it takes about 0.001 seconds to spawn a thread on my system, so the drift for runPeriodicallySmallDrift after n iterations is about n thousandths of a second, which may be negligible in some use cases. 在我的实验中(下面有更多细节),在我的系统上生成一个线程需要大约0.001秒,因此在n次迭代之后runPeriodicallySmallDrift的漂移大约是千分之一秒,这在某些用例中可以忽略不计。

Final Solution: runPeriodicallyConstantDrift 最终解决方案: runPeriodicallyConstantDrift

Finally, suppose we require only constant drift, meaning the drift is always less than some constant, and does not grow with the number of iterations of the periodic action. 最后,假设我们只需要恒定漂移,这意味着漂移总是小于某个常数,并且不会随着周期动作的迭代次数而增长。 We can achieve constant drift by keeping track of the total time since we started, and starting the n th iteration when the total time is n times the period: 我们可以通过跟踪自开始以来的总时间来实现恒定漂移,并且当总时间是周期的n倍时开始第n次迭代:

import           Control.Concurrent ( threadDelay )
import           Data.Time.Clock.POSIX ( getPOSIXTime )
import           Text.Printf ( printf )

-- | Run @action@ every @period@ seconds.
runPeriodicallyConstantDrift :: Double -> IO () -> IO ()
runPeriodicallyConstantDrift period action = do
  start <- getPOSIXTime
  go start 1
  where
    go start iteration = do
      action
      now <- getPOSIXTime
      -- Current time.
      let elapsed = realToFrac $ now - start
      -- Time at which to run action again.
      let target = iteration * period
      -- How long until target time.
      let delay = target - elapsed
      -- Fail loudly if the action takes longer than one period.  For
      -- some use cases it may be OK for the action to take longer
      -- than one period, in which case remove this check.
      when (delay < 0 ) $ do
        let msg = printf "runPeriodically: action took longer than one period: delay = %f, target = %f, elapsed = %f"
                  delay target elapsed
        error msg
      threadDelay (round $ delay * microsecondsInSecond)
      go start (iteration + 1)
    microsecondsInSecond = 10 ** 6

Based on experiments below, the drift is always about 1/1000th of a second, independent of the number of iterations of the action. 基于以下实验,漂移总是大约1/1000秒,与动作的迭代次数无关。

Comparison Of Solutions By Testing 通过测试比较解决方案

To compare these solutions, we create an action that keeps track of its own drift and tells us, and run it in each of the runPeriodically* implementations above: 为了比较这些解决方案,我们创建了一个动作来跟踪它自己的漂移并告诉我们,并在上面的每个runPeriodically*实现中运行它:

import           Control.Concurrent ( threadDelay )
import           Data.IORef ( newIORef, readIORef, writeIORef )
import           Data.Time.Clock.POSIX ( getPOSIXTime )
import           Text.Printf ( printf )

-- | Use a @runPeriodically@ implementation to run an action
-- periodically with period @period@. The action takes
-- (approximately) @runtime@ seconds to run.
testRunPeriodically :: (Double -> IO () -> IO ()) -> Double -> Double -> IO ()
testRunPeriodically runPeriodically runtime period = do
  iterationRef <- newIORef 0
  start <- getPOSIXTime
  startRef <- newIORef start
  runPeriodically period $ action startRef iterationRef
  where
    action startRef iterationRef = do
      now <- getPOSIXTime
      start <- readIORef startRef
      iteration <- readIORef iterationRef
      writeIORef iterationRef (iteration + 1)
      let drift = (iteration * period) - (realToFrac $ now - start)
      printf "test: iteration = %.0f, drift = %f\n" iteration drift
      threadDelay (round $ runtime * 10**6)

Here are the test results. 以下是测试结果。 In each case test an action that runs for 0.05 seconds, and use a period of twice that, ie 0.1 seconds. 在每种情况下,测试运行0.05秒的动作,并使用两倍的时间,即0.1秒。

For runPeriodicallyBigDrift , the drift after n iterations is about n times the runtime of a single iteration, as expected. 对于runPeriodicallyBigDrift ,n次迭代后的漂移大约是单次迭代的运行时间的n倍,如预期的那样。 After 100 iterations the drift is -5.15, and the predicted drift just from runtime of the action is -5.00: 在100次迭代之后,漂移为-5.15,并且仅从动作的运行时间预测的漂移为-5.00:

ghci> testRunPeriodically runPeriodicallyBigDrift 0.05 0.1
...
test: iteration = 98, drift = -5.045410253
test: iteration = 99, drift = -5.096661091
test: iteration = 100, drift = -5.148137684
test: iteration = 101, drift = -5.199764033999999
test: iteration = 102, drift = -5.250980596
...

For runPeriodicallySmallDrift , the drift after n iterations is about 0.001 seconds, presumably the time it takes to spawn a thread on my system: 对于runPeriodicallySmallDrift ,n次迭代后的漂移大约为0.001秒,可能是在我的系统上生成线程所需的时间:

ghci> testRunPeriodically runPeriodicallySmallDrift 0.05 0.1
...
test: iteration = 98, drift = -0.08820333399999924
test: iteration = 99, drift = -0.08908210599999933
test: iteration = 100, drift = -0.09006684400000076
test: iteration = 101, drift = -0.09110764399999915
test: iteration = 102, drift = -0.09227584299999947
...

For runPeriodicallyConstantDrift , the drift remains constant (plus noise) at about 0.001 seconds: 对于runPeriodicallyConstantDrift ,漂移在大约0.001秒内保持不变(加上噪声):

ghci> testRunPeriodically runPeriodicallyConstantDrift 0.05 0.1
...
test: iteration = 98, drift = -0.0009586619999986112
test: iteration = 99, drift = -0.0011010979999994674
test: iteration = 100, drift = -0.0011610369999992542
test: iteration = 101, drift = -0.0004908619999977049
test: iteration = 102, drift = -0.0009897379999994627
...

If we cared about that level of constant drift, then a more sophisticiated solution could track the average constant drift and adjust for it. 如果我们关心恒定漂移的水平,那么更复杂的解决方案可以跟踪平均恒定漂移并对其进行调整。

Generalization To Stateful Periodic Loops 对有状态周期循环的推广

In practice I realized that some of my loops have state that passes from one iteration to the next. 在实践中,我意识到我的一些循环具有从一次迭代传递到下一次迭代的状态。 Here's a slight generalization of runPeriodicallyConstantDrift to support that: 这是runPeriodicallyConstantDrift的一个小概括,以支持:

import           Control.Concurrent ( threadDelay )
import           Data.IORef ( newIORef, readIORef, writeIORef )
import           Data.Time.Clock.POSIX ( getPOSIXTime )
import           Text.Printf ( printf )

-- | Run a stateful @action@ every @period@ seconds.
--
-- Achieves uniformly bounded drift (i.e. independent of the number of
-- iterations of the action) of about 0.001 seconds,
runPeriodicallyWithState :: Double -> st -> (st -> IO st) -> IO ()
runPeriodicallyWithState period st0 action = do
  start <- getPOSIXTime
  go start 1 st0
  where
    go start iteration st = do
      st' <- action st
      now <- getPOSIXTime
      let elapsed = realToFrac $ now - start
      let target = iteration * period
      let delay = target - elapsed
      -- Warn if the action takes longer than one period. Originally I
      -- was failing in this case, but in my use case we sometimes,
      -- but very infrequently, take longer than the period, and I
      -- don't actually want to crash in that case.
      when (delay < 0 ) $ do
        printf "WARNING: runPeriodically: action took longer than one period: delay = %f, target = %f, elapsed = %f"
          delay target elapsed
      threadDelay (round $ delay * microsecondsInSecond)
      go start (iteration + 1) st'
    microsecondsInSecond = 10 ** 6

-- | Run a stateless @action@ every @period@ seconds.
--
-- Achieves uniformly bounded drift (i.e. independent of the number of
-- iterations of the action) of about 0.001 seconds,
runPeriodically :: Double -> IO () -> IO ()
runPeriodically period action =
  runPeriodicallyWithState period () (const action)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM