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

Haskell 如何在GHCJS程序中定期执行操作?,haskell,concurrency,ghcjs,Haskell,Concurrency,Ghcjs,应该通过Javascript使用setInterval,还是使用基于线程的更惯用的解决方案?使用setInterval,Alexander、Erik和Luite本人的评论引导我尝试线程。这可以无缝地工作,代码非常简洁,如下所示: import Control.Concurrent( forkIO, threadDelay ) import Control.Monad( forever ) ... within an IO block threadId <- forkIO $ foreve

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

使用
setInterval
,Alexander、Erik和Luite本人的评论引导我尝试线程。这可以无缝地工作,代码非常简洁,如下所示:

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
import Control.Concurrent(forkIO,threadDelay)
进口管制.单子(永远)
... 在IO块内

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

我的答案不是针对GHCJS的,也没有在GHCJS上测试过,只有GHC,但它说明了存在的问题

第一个斯特劳曼解决方案:
runPeriodicallyBigDrift
以下是我的OP解决方案版本,供比较:

import Control.Concurrent(线程延迟)
进口管制.单子(永远)
--|每@period@秒运行@action@一次。
runPeriodicallyBigDrift::Double->IO()->IO()
runperiodicalybigdrift period action=forever$do
行动
线程延迟(四舍五入$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)
根据下面的实验,漂移始终约为1/1000秒,与动作的迭代次数无关

通过测试比较解决方案 为了比较这些解决方案,我们创建了一个动作来跟踪它自己的漂移并告诉我们,并在上面的每个
runperiodic*
实现中运行它:

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)
对于
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)
import Control.Concurrent(线程延迟)
导入Data.IORef(newIORef、readIORef、writeIORef)
导入Data.Time.Clock.POSIX(getPOSIXTime)
导入Text.Printf(Printf)
--|每@period@秒运行一次有状态的@action@操作。
--
--实现一致有界漂移(即独立于
--动作的迭代)约0.001秒,
runPeriodicallyWithState::Double->st->(st->IO st)->IO()
runperiodicalyWithState period st0 action=do

如果你只针对浏览器,那么我认为这只是品味的问题。当使用GHCJS编译时,使用
threadDelay
的解决方案将起作用。A团队鉴于您投入的大量研究和实验,我将您的答案标记为最佳解决方案。。。但老实说,我没有时间去了解所有的动机和整个线索。我认为仅仅设置线程延迟是不精确的。。。必须有一种更简单的方法以给定的时间间隔启动线程,即使这可能会导致更多这样的线程同时运行。否则,我认为我更喜欢下一个线程的执行被延迟一点的解决方案。同样,如果你在文章的开头提出你的解决方案,并在后面为感兴趣的人添加动机和研究,那么它会更具可读性。。。但就我所见,你的最终解决方案涉及到很多复杂性,许多读者仍然更倾向于选择一个不着边际的解决方案time@danza关于最终解决方案的复杂性,您是否查看了
runPeriodicallySmallDrift
版本?这个版本非常简单,但也保持了相对较小的漂移。@danza我在注释中提到,如果你不首先考虑动机,就跳到最终解决方案,并在最终解决方案中添加了更多的注释和散文。看看它现在是否有意义。@danza,我相信研究和构建这个答案所花费的时间比你仔细阅读它所花费的时间要长得多。你上面的评论会让人们对花时间回答你的问题三思而后行。
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
...
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)