Observable.Timer():如何避免定时器漂移?

在C#(.NET 4.0)应用程序中,我使用Reactive Extensions(2.0.20823.0)生成时间边界,以便将事件分组为聚合值。 为了简化对生成的数据库的查询,需要在整个小时(或下面示例中的秒)对齐这些边界。

使用Observable.Timer()

 var time = DefaultScheduler.Instance; var start = new DateTimeOffset(time.Now.DateTime, time.Now.Offset); var span = TimeSpan.FromSeconds(1); start -= TimeSpan.FromTicks(start.Ticks % 10000000); start += span; var boundary = Observable.Timer(start, span, time); boundary.Select(i => start + TimeSpan.FromSeconds(i * span.TotalSeconds)) .Subscribe(t => Console.WriteLine("ideal: " + t.ToString("HH:mm:ss.fff"))); boundary.Select(i => time.Now) .Subscribe(t => Console.WriteLine("actual: " + t.ToString("HH:mm:ss.fff"))); 

您可以看到计时器的预期和实际时间相差很大:

 ideal: 10:06:40.000 actual: 10:06:40.034 actual: 10:06:41.048 ideal: 10:06:41.000 actual: 10:06:42.055 ideal: 10:06:42.000 ideal: 10:06:43.000 actual: 10:06:43.067 actual: 10:06:44.081 ideal: 10:06:44.000 ideal: 10:06:45.000 actual: 10:06:45.095 actual: 10:06:46.109 ideal: 10:06:46.000 ideal: 10:06:47.000 actual: 10:06:47.123 actual: 10:06:48.137 ideal: 10:06:48.000 ... 

我也使用了HistoricalScheduler ,当然我没有问题。 我可以容忍轻微的不准确,我不需要关心系统时钟的变化。 这些Observables没有触发重量级操作。

此外,我知道在这篇博文中有一个关于RX定时器漂移问题的冗长讨论,但我似乎无法绕过它。

如果没有系统的定时器漂移,定期安排Observable的正确方法是什么?

你可以使用Observable.Generate :

 var boundary = Observable.Generate( 0, _ => true, // start condition i => ++i, // iterate i => i, // result selector i => start + TimeSpan.FromSeconds(i * span.TotalSeconds), time); 

这将基于每次迭代的绝对时间重新安排。

这是一些示例输出:

 actual: 01:00:44.003 ideal: 01:00:44.000 actual: 01:00:44.999 ideal: 01:00:45.000 actual: 01:00:46.012 ideal: 01:00:46.000 actual: 01:00:47.011 ideal: 01:00:47.000 actual: 01:00:48.011 ideal: 01:00:48.000 actual: 01:00:49.007 ideal: 01:00:49.000 actual: 01:00:50.009 ideal: 01:00:50.000 actual: 01:00:51.006 ideal: 01:00:51.000 

它完全不匹配,我想是由于汉斯解释的原因,但没有漂移。

编辑:

以下是RxSource的一些评论

 // BREAKING CHANGE v2 > v1.x - No more correction for time drift based on absolute time. This // didn't work for large period values anyway; the fractional // error exceeded corrections. Also complicated dealing with system // clock change conditions and caused numerous bugs. // // - For more precise scheduling, use a custom scheduler that measures TimeSpan values in a // better way, eg spinning to make up for the last part of the period. Whether or not the // values of the TimeSpan period match NT time or wall clock time is up to the scheduler. // // - For more accurate scheduling wrt the system clock, use Generate with DateTimeOffset time // selectors. When the system clock changes, intervals will not be the same as diffs between // consecutive absolute time values. The precision will be low (1s range by default). 

大多数机器上的默认Windows时钟中断率是每秒64个中断。 由CLR环绕到15.6毫秒。 如果你要求1000毫秒的间隔,这不是一个快乐的数字,没有整数除数。 最接近的匹配是64 x 15.6 = 998(太短)和65 x 15.6 = 1014毫秒。

这正是你所看到的,41.048 – 40.034 = 1.014。 44.081 – 43.067 = 1.014,等等。

你实际上可以改变中断率,你可以调用timeBeginPeriod()并要求1毫秒的间隔。 在程序终止时你需要timeEndPeriod()来重置它。 这不是一个非常合理的事情,它具有系统范围的副作用,并且对功耗非常不利。 但会解决你的问题。

一个更理智的方法是承认你永远不能通过增加间隔来准确地保持时间。 CLR使用的15.6毫秒已经是近似值。 始终使用绝对时钟重新校准。 通过要求998毫秒而不是1000来更近距离.Etcetera。