【问题标题】:How to execute an action periodically in a GHCJS program?如何在 GHCJS 程序中定期执行操作?
【发布时间】:2016-02-10 05:14:28
【问题描述】:

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

【问题讨论】:

  • 如果您只针对浏览器,那么我认为这只是个人喜好问题。使用threadDelay 的解决方案在使用 GHCJS 编译时起作用。

标签: haskell concurrency ghcjs


【解决方案1】:

如果您不关心动机,只需滚动到下面我的最佳解决方案runPeriodicallyConstantDrift如果您喜欢更简单但结果更差的解决方案,请参阅runPeriodicallySmallDrift

我的答案不是 GHCJS 特定的,也没有在 GHCJS 上测试过,只有 GHC,但它说明了 the OP's naive solution 的问题。

第一个稻草人解决方案:runPeriodicallyBigDrift

这是我的 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)

假设“定期执行一个动作”意味着动作开始每隔几秒,OP的解决方案是有问题的,因为threadDelay没有考虑动作本身的时间需要。在 n 次迭代之后,动作的开始时间至少会偏移运行 n 次所需的时间!

第二个稻草人解决方案:runPeriodicallySmallDrift

所以,如果我们真的想在每个周期开始一个新动作,我们需要考虑动作运行所花费的时间。如果与生成线程所需的时间相比,周期相对较大,那么这个简单的解决方案可能对您有用:

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)

在我的实验中(下面有更多详细信息),在我的系统上生成一个线程大约需要 0.001 秒,因此在 n 次迭代后 runPeriodicallySmallDrift 的漂移大约是千分之 n 秒,在某些用途中可以忽略不计案例。

最终解决方案:runPeriodicallyConstantDrift

最后,假设我们只需要常数漂移,这意味着漂移总是小于某个常数,并且不会随着周期性动作的迭代次数而增长。我们可以通过跟踪自开始以来的总时间来实现恒定漂移,并在总时间为 n 乘以周期时开始 nth 迭代:

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

根据以下实验,漂移始终约为 1/1000 秒,与动作的迭代次数无关。

通过测试比较解决方案

为了比较这些解决方案,我们创建了一个动作来跟踪它自己的漂移并告诉我们,并在上面的每个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)

这是测试结果。在每种情况下测试一个运行 0.05 秒的操作,并使用两倍的时间,即 0.1 秒。

对于runPeriodicallyBigDrift,n 次迭代后的漂移大约是单次迭代运行时间的 n 倍,正如预期的那样。 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
...

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

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

如果我们关心这种水平的恒定漂移,那么更复杂的解决方案可以跟踪平均恒定漂移并进行调整。

有状态周期性循环的泛化

在实践中,我意识到我的一些循环具有从一个迭代传递到下一个迭代的状态。以下是对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)

【讨论】:

  • aheam 鉴于您投入的大量研究和实验,我将您的答案标记为最佳解决方案……但老实说,我没有时间了解所有动机和整体线。我认为仅设置线程延迟并不精确......必须有一种更简单的方法来以给定的时间间隔启动线程,即使认为这可能会导致更多这样的线程同时运行。否则我认为我更喜欢下一个线程的执行延迟一点的解决方案
  • 另外,如果您在文本的开头提出您的解决方案,并在之后为感兴趣的人添加动机和研究......但据我所知,您的最终解决方案涉及很多复杂性,许多读者仍然会更喜欢随时间漂移的那个
  • @danza 关于最终解决方案很复杂,你看过runPeriodicallySmallDrift 版本吗?该版本非常简单,但漂移相对较小。
  • @danza 如果您首先不关心动机,我会写下关于跳转到最终解决方案的注释,并在最终解决方案中添加更多 cmets 和散文。现在看看是否有意义。
  • @danza,我相信研究和构建这个答案所花费的时间比你仔细阅读它所花费的时间要长得多。您在上面制作的 cmets 会让人们在花时间回答您的问题时三思而后行。
【解决方案2】:

使用setInterval pose some challenges 和来自 Alexander、Erik 和 Luite 的 cmets 引导我尝试线程。这可以无缝运行,代码非常简洁,类似于以下内容:

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 具有轻量级线程的概念,因此这是 Haskell 惯用的异步方式运行操作,就像使用 Javascript setIntervalsetTimeout 一样。

【讨论】:

    猜你喜欢
    • 2015-05-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-19
    • 2010-11-05
    • 1970-01-01
    • 2011-08-18
    • 1970-01-01
    相关资源
    最近更新 更多