创建一个精确的时间间隔,在很长一段时间内没有漂移
Posted
技术标签:
【中文标题】创建一个精确的时间间隔,在很长一段时间内没有漂移【英文标题】:Creating a precise time interval with no drift over long periods of time 【发布时间】:2021-04-30 06:41:32 【问题描述】:我需要运行一个长时间运行的任务,该任务仍处于循环中。循环中的代码只能在相同的时间间隔内执行。处理此问题的最常见方法可能是使用间隔计时器。间隔计时器的问题在于它不准确。如果我在计时器触发时绘制绝对时间,那么在很长一段时间内会有少量漂移。如果间隔时间相当大,例如数百毫秒或更多,这不是问题。但是如果间隔只有几十毫秒,就成了问题。
每当出现时间间隔时,该间隔必须相对于进入循环之前的起点发生。它不能是从最后一个结束时开始的时间间隔,否则您可能会随着时间的推移而累积漂移。
还有一个问题,如果定时器块中的代码在时间间隔内完成的时间太长,一旦间隔到期,我不确定,但我相信定时器会立即启动。这会导致从一个循环到下一个循环的时间间隔不均匀。
我能想到的唯一解决方案是在循环开始之前记录系统时钟。我将此称为“开始时间”。它只存在一次并且永远不会被覆盖。然后在循环的代码完成后,测量当前时间和开始时间的时间差,并通过将差除以我们希望循环执行的速率来确定我们所处的时间间隔。如果尚未达到当前时间间隔,则要么休眠直到时间到期,要么继续循环并测量时间差,直到当前间隔到期。
是否有更好的解决方案?
【问题讨论】:
“间隔计时器”到底是什么意思? 执行循环中的代码,比如每 30 毫秒。时间间隔为 30 毫秒。 你选择Java来实现这个? Java 或 Kotlin。这是一个需要处理此问题的 android 应用程序。 我听起来好像与线程调度(睡眠、调度线程执行)有关的任何事情都可能不够精确。另一种方法是不断循环旋转,几乎按照您的描述检查时间,然后执行或继续旋转。就 CPU 而言,这可能很昂贵,但是,您确实想要精细控制。 【参考方案1】:ScheduledExecutorSerivce
如果我理解您的问题,而我很可能不理解,您只是在询问如何按照严格的时间表为重复的任务计时,因为该任务的执行需要时间,因此可能会超出时间表。
如果我理解正确,那么您正在讨论两种ScheduledExecutorService
方法之间的有效区别:
scheduleAtFixedRate
scheduleWithFixedDelay
如果您希望任务在某一分钟开始,并且每分钟运行一次,您将首先计算一个初始延迟,直到下一分钟到来。然后,您将再次调用第一个方法 scheduleAtFixedRate
,以计划自第一分钟以来经过的下一分钟,而不是任务执行完成时经过的时间。
如果您想等待任务执行完成后再开始执行任务,请调用第二种方法scheduleWithFixedDelay
。
漂移是不可避免的
正如ScheduledExecutorService
的 Javadoc 中所述,您不能期望在传统硬件和传统操作系统上运行的传统 Java 应用程序中没有漂移的完美时机。
如果你想要几十毫秒的节奏,不随时间漂移,我想你会失望的。进程和线程通常会以不可预知的方式被操作系统和硬件以不可预知的时间暂停,从而中断您精心安排的任务的运行。在 JVM 中,垃圾收集和其他事件也可能会以不可预知的方式暂停您精心计时的任务。
如果你真的需要这样的精度,你应该研究real time Java 的实现。
示例代码
我尝试了以下代码作为实验。
我尝试通过运行一次任务来预热执行器服务,然后等待几秒钟。
为了更容易阅读时间戳,我的意图是在下一分钟开始重复任务。我的代码表现不佳,延迟了 6 毫秒。例如:Now: 2021-04-30T07:39:00.006474Z
。
之后,预定的执行器服务在保持 10 毫秒的节奏方面做得很好。在下面的示例中,我们达到了 12 ms、23 ms、33、43、52、63、73、83、92、103 等。我现在没有时间再追究它,以观察一个多小时的漂移。
// Prepare the background thread.
final long fixedRateInNanos = Duration.ofMillis( 10 ).toNanos();
Runnable runnable = ( ) -> System.out.println( "Now: " + Instant.now() );
ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
ses.submit( runnable ); // Warm the executor service and backing thread pool.
try Thread.sleep( Duration.ofSeconds( 2 ).toMillis() ); catch ( InterruptedException e ) e.printStackTrace();
// Calculate the schedule.
ZoneId z = ZoneId.of( "America/Edmonton" );
ZonedDateTime now = ZonedDateTime.now( z );
Duration d = Duration.between( now , now.truncatedTo( ChronoUnit.MINUTES ).plusMinutes( 1 ) );
long initialDelayInNanos = d.toNanos();
ses.scheduleAtFixedRate( runnable , initialDelayInNanos , fixedRateInNanos , TimeUnit.NANOSECONDS );
// Wait a while for app to run.
try Thread.sleep( Duration.ofMinutes( 2 ).toMillis() ); catch ( InterruptedException e ) e.printStackTrace();
ses.shutdown();
try ses.awaitTermination( Duration.ofSeconds( 10).toMillis() , TimeUnit.MILLISECONDS ); catch ( InterruptedException e ) e.printStackTrace();
System.out.println( "INFO - `main` done running. " + Instant.now() );
在 macOS 10.14.6 Mojave 上使用 Java 16 从 IntelliJ 2021.1.1 RC 运行时,在具有 6 个真正 Intel 内核且无超线程的 Mac mini 上运行。
Now: 2021-04-30T07:38:51.444455Z
Now: 2021-04-30T07:39:00.006474Z
Now: 2021-04-30T07:39:00.012990Z
Now: 2021-04-30T07:39:00.023869Z
Now: 2021-04-30T07:39:00.033129Z
Now: 2021-04-30T07:39:00.043817Z
Now: 2021-04-30T07:39:00.052992Z
Now: 2021-04-30T07:39:00.063646Z
Now: 2021-04-30T07:39:00.073201Z
Now: 2021-04-30T07:39:00.083842Z
Now: 2021-04-30T07:39:00.092991Z
Now: 2021-04-30T07:39:00.103702Z
Now: 2021-04-30T07:39:00.113141Z
Now: 2021-04-30T07:39:00.123775Z
Now: 2021-04-30T07:39:00.132995Z
Now: 2021-04-30T07:39:00.143679Z
Now: 2021-04-30T07:39:00.153165Z
Now: 2021-04-30T07:39:00.163813Z
Now: 2021-04-30T07:39:00.172826Z
Now: 2021-04-30T07:39:00.183647Z
Now: 2021-04-30T07:39:00.193121Z
Now: 2021-04-30T07:39:00.203783Z
【讨论】:
不能肯定,但我高度怀疑 scheduleAtFixedRate 会导致长时间漂移。它没有现实世界时间的概念,只是在某个时间间隔触发,而不考虑现实世界的时钟,并且该时间间隔在时间上不是一个一致的值。 @AndroidDev 真。我再写一点来解释你避免漂移的意图是不现实的。 避免漂移是非常现实的,并且可以实现,至少在处理一个小时内运行的毫秒时间间隔时,至少在可接受的范围内。我认为唯一的解决方案是确保时间间隔与系统时钟绑定。 操作系统可能会暂停一个线程,但最终一旦一个线程再次开始运行,只要它与系统时钟绑定并且系统时钟高度准确,时间间隔就会准确。您可能仍会在任何特定时间间隔内获得交易,但相对于实时不会有漂移。我没有提到我的代码将在移动设备上运行,其中系统时钟基本上等同于原子钟,具有极高的准确性。 GPS 应用程序需要高度精确的时钟才能使用并依赖于系统时钟。 @AndroidDev:需要这种精度的应用程序不能在具有通用编程语言的通用操作系统上运行。他们使用实时操作系统和特定语言(或语言变体)来获得保证。是的,您可能可以在一些可接受的余量内达到您的目标,但我们不知道您认为哪种余量。【参考方案2】:我通常这样做的方式是记录下一个间隔的时间,然后进行睡眠或其他任何尝试并击中它的方法。当循环恢复时,将当前时间与目标时间进行比较:
val difference = targetTime - currentTime
if (difference > 0) sleep(difference)
else
// do thing
val newTarget = targetTime + interval
// get current time again because you've spent time doing thing
sleep(newTarget - newCurrent)
// or check it's not < 1 i.e. you overshot, so just loop again
所以每个目标时间只是多次添加interval
的结果,与循环实际运行的时间无关。您只需要依靠准确的时钟即可。
要注意的另一件事是您的实际间隔 - 如果它是一个不错的整数,那么您会没事的,如果您将其计算为分数(例如 1/30 秒),您将有浮点数学中的不准确性,并且随着您不断添加稍微错误的数字,这些会变得更加复杂。因此,您可能需要每隔一段时间在某些“里程碑”处重新计算从开始时间开始的目标时间,因此您需要重新设置准确性。
【讨论】:
以上是关于创建一个精确的时间间隔,在很长一段时间内没有漂移的主要内容,如果未能解决你的问题,请参考以下文章