2014-11-21 112 views
1

我试图控制对一个对象的访问,以便它可能只在给定的时间范围内被访问一定次数。在我有一个单元测试中,访问限制为每秒一次。所以5次访问应该只需4秒钟。但是,测试在我们的TFS服务器上失败,仅需2秒钟。我的代码的精简版本是这样的:C#编译器优化循环?

public class RateLimitedSessionStrippedDown<T> 
{ 
    private readonly int _rateLimit; 
    private readonly TimeSpan _rateLimitSpan; 
    private readonly T _instance; 
    private readonly object _lock; 

    private DateTime _lastReset; 
    private DateTime _lastUse; 
    private int _retrievalsSinceLastReset; 

    public RateLimitedSessionStrippedDown(int limitAmount, TimeSpan limitSpan, T instance) 
    { 
     _rateLimit = limitAmount; 
     _rateLimitSpan = limitSpan; 
     _lastUse = DateTime.UtcNow; 
     _instance = instance; 
     _lock = new object(); 
    } 

    private void IncreaseRetrievalCount() 
    { 
     _retrievalsSinceLastReset++; 
    } 

    public T GetRateLimitedSession() 
    { 
     lock (_lock) 
     { 
      _lastUse = DateTime.UtcNow; 

      Block(); 

      IncreaseRetrievalCount(); 

      return _instance; 
     } 
    } 

    private void Block() 
    { 
     while (_retrievalsSinceLastReset >= _rateLimit && 
      _lastReset.Add(_rateLimitSpan) > DateTime.UtcNow) 
     { 
      Thread.Sleep(TimeSpan.FromMilliseconds(10)); 
     } 

     if (DateTime.UtcNow > _lastReset.Add(_rateLimitSpan)) 
     { 
      _lastReset = DateTime.UtcNow; 
      _retrievalsSinceLastReset = 0; 
     } 
    } 
} 

在我的电脑上运行时,在Debug和Release中,它都能正常工作。但是,我有一个单元测试失败,一旦我提交到我们的TFS构建服务器。这是测试:

[Test] 
    public void TestRateLimitOnePerSecond_AssertTakesAtLeastNMinusOneSeconds() 
    { 
     var rateLimiter = new RateLimitedSessionStrippedDown<object>(1, TimeSpan.FromSeconds(1), new object()); 

     DateTime start = DateTime.UtcNow; 

     for (int i = 0; i < 5; i++) 
     { 
      rateLimiter.GetRateLimitedSession(); 
     } 

     DateTime end = DateTime.UtcNow; 

     Assert.GreaterOrEqual(end.Subtract(start), TimeSpan.FromSeconds(4)); 
    } 

Test failure from the TFS CI Build Server

我不知道如果在测试循环中,它运行在一个单独的线程(或类似的东西),循环的每次迭代的方式不断优化其意味着测试比它应该更快地完成,因为Thread.Sleep只会阻止它被调用的线程?

+0

关于代码有一些奇怪的地方 - 比如,我不明白为什么你有'_lastUse',并且最好使用'Stopwatch'而不是'DateTime.UtcNow'来跟踪时间 - 但我没有看到任何可以解释问题的明显错误。唯一想到的是可能性(因为你没有使用'Stopwatch'),如果由于某种原因系统时间无法更新,时钟时间将错误地测量实际的经过时间。这很不可能与编译器优化有关,特别是涉及线程的优化。 – 2014-11-21 22:57:20

+0

问自己当_lastReset.Add(_rateLimitSpan)== DateTime.UtcNow'时会发生什么。另外,对于这个写得很好的代码,依赖'_lastReset = default(DateTime)'初始化是很好奇和不透明的。此外,真正的代码不应该'睡眠()'。考虑等待Waithandle或其他东西。 – 2014-11-21 23:06:08

+0

这个测试失败的可能性虽然不大但在理论上可行,但如果在运行测试的同时发生与时间服务器的同步(并且您无法控制该测试),那么测试失败的方式是可行的。但是,如果这实际上是问题,那将是非常令人惊讶的。 – hvd 2014-11-22 00:20:29

回答

3

你的问题是Block方法里面,现在我看的评论,似乎亨克Holterman已经使这件事。

_lastReset.Add(_rateLimitSpan)DateTime.UtcNow相等它只会失败。这并不经常发生,因此间歇性失败的原因。一个修复程序是改变>>=在这条线:

if (DateTime.UtcNow > _lastReset.Add(_rateLimitSpan)) 

这不是直观的原因,除非你明白,每次调用DateTime.UtcNow不一定返回一个新值,一个每次通话。

即使DateTime.UtcNow精确到100纳秒,其精度与精度不一样。它依赖于机器的定时器间隔,其范围从1-15毫秒,但更经常设置为15.25毫秒,除非你正在使用多媒体。

您可以在行动与此dotnetfiddle看到这一点。除非您打开一个程序,将计时器设置为不同的值(如1ms),否则您会注意到计时器之间的差异大约为150000个计时器,大约15ms或正常的系统计时器间隔。

我们还可以通过解除了调用DateTime.UtcNow到临时变量,并将它们在方法的结尾比较验证这一点:

private void Block() 
    { 
     var first = DateTime.UtcNow; 
     while (_retrievalsSinceLastReset >= _rateLimit && 
      _lastReset.Add(_rateLimitSpan) > first) 
     { 
      Thread.Sleep(TimeSpan.FromMilliseconds(10)); 
      first = DateTime.UtcNow; 
     } 

     var second = DateTime.UtcNow; 
     if (second > _lastReset.Add(_rateLimitSpan)) 
     { 
      _lastReset = DateTime.UtcNow; 
      _retrievalsSinceLastReset = 0; 
     } 

     if (first == second) 
     { 
      Console.WriteLine("DateTime.UtcNow returned same value"); 
     } 
    } 

在我的机器,所有五个呼叫Block打印出来DateTime.UtcNow为等于。