等待超过15毫秒的Task.Delay不够长。

3
在排除验证并发性的测试问题时,我发现我们一些运行在虚拟机中的构建机会经常报告任务没有等待完整的 Task.Delay 时间间隔。为了确认这个问题,我编写了一个测试脚本创建一个直方图,记录请求延迟时间到实际等待时间的差异。在我的机器上,结果如预期所示:
interval ms => number of items completing in that interval
--
0 - 4 ms => 13 # 13 iterations completed within 4 ms of the requested delay
5 - 9 ms => 194
10 - 14 ms => 714
15 - 19 ms => 61
20 - 24 ms => 12
25 - 29 ms => 3
40 - 44 ms => 2
45 - 49 ms => 1

但是在构建机上,测试报告显示所有任务在请求的延迟完成之前都已完成:

-10 - -6 ms => 999
-5 - -1 ms => 1

这里是测试/直方图生成器:

[Test]
[Timeout(60_000)]
public async Task TaskDelayAccuracyCheck()
{
  var results = new List<long>();
  for (var i = 0; i<1000; ++i)
  {
    var sw = new Stopwatch();
    sw.Start();
    await Task.Delay(20);
    sw.Stop();
    results.Add((sw.ElapsedTicks - 20*10_000)/10_000);
  }
  var histo = results.GroupBy(t => t / 5).OrderBy(x => x.Key);
  foreach (var group in histo)
  {
    Console.WriteLine($"{group.Key*5} - {(group.Key+1)*5 - 1} ms => {group.Count()}");
  }
  Assert.Multiple(() =>
  {
    foreach (var group in histo)
    {
      Assert.That(group.Key, Is.GreaterThanOrEqualTo(0));
    }
  });
}

Task.Delay文档中指出(已加粗标识):

此方法依赖于系统时钟。这意味着,如果延迟参数小于系统时钟的分辨率,则时间延迟将大约等于系统时钟的分辨率,Windows 系统上大约为 15 毫秒。

在这种情况下,我保证延迟时间不会小于上述的15毫秒,并使用了“Stopwatch”类来确保计时尽可能精确。此外,“Stopwatch.IsHighResolution”返回“True”,因此我应该能够信任计时。
我一直认为延迟总是至少符合请求的时间,但可能会因系统时钟的分辨率而变长。现在应该推断出延迟始终会在系统时钟的分辨率(即大约15毫秒)内吗?还是其他情况?

构建服务器和您的本地机器之间可能存在环境差异吗?您说“一些”构建机器报告这些奇怪的结果 - 我假设您的一些构建机器正在报告感知正确的结果? - Victor Wilson
如果您需要一个准确的“计时器”,您应该始终使用一个新线程,并且如果延迟较小(<30毫秒)并且必须准确,则使用thread.sleep()。如果您正在使用任务,则使用方便的路线,并且始终取决于线程池在请求时是否提供空闲线程。 - Charles
3
我认为你的计算涉及ElapsedTicks是不正确的。你能解释一下那个计算式从哪里来吗?使用秒表的Elapsed.TotalMilliseconds会更容易些。 - Mike Zboray
1
Stopwatch.Frequency 在同一台机器上返回什么?如果不是 10000000,那么就像其他人所说,你的计算是错误的。 - sellotape
2
代码假定一个时钟周期为100纳秒,就像DateTime一样。但实际上并不是这样的。请使用Elapsed.Ticks代替。 - Hans Passant
我曾认为所有“高分辨率”计时器的频率都是10,000,000,但实际上并非如此,这导致了我的懒惰追上了我。在构建机器上,即使将“IsHighResolution”设置为true,我仍然看到2,438,582的频率。 - Kaleb Pederson
1个回答

4

我认为你的测量方法有误。

你假设每毫秒总是有10,000个滴答。但这并不总是正确的。您可以查看 Stopwatch.Frequency 来查看当前系统上使用的每秒滴答数。该值是基于对本机 Windows QueryPerformanceFrequency 函数的调用,第一次使用 Stopwatch 时设置

除以1000可得到每毫秒的滴答数。

var ticksPerMillisecond = Stopwatch.Frequency / 1000;

但是你甚至可以更容易地使用ElapsedMilliseconds属性,它会精确地将滴答数转换为毫秒

results.Add(sw.ElapsedMilliseconds - 20);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接