Skip to main content
 首页 » 编程设计

C# 编译器优化循环

2024年08月12日48dudu

我正在尝试控制对对象的访问,以便在给定时间跨度内只能访问特定次数。在我进行的一个单元测试中,访问限制为每秒一次。所以 5 次访问应该只需要 4 秒多一点。然而,测试在我们的 TFS 服务器上失败了,只用了 2 秒。我的代码的精简版本在这里:

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; 
        } 
    } 
} 

在我的计算机上运行时,在调试和发布中,它都工作正常。但是,一旦我提交给我们的 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)); 
    } 

我想知道测试中的循环是否以在单独的线程(或类似的东西)上运行循环的每个迭代的方式进行了优化,这意味着测试比它应该完成的更快,因为 Thread.Sleep only阻塞正在调用它的线程?

请您参考如下方法:

您的问题出在 Block 方法内部,现在我看了评论,Henk Holterman 似乎已经提出了这个问题。

只有当 _lastReset.Add(_rateLimitSpan)DateTime.UtcNow 相等时才会失败。这不会经常发生,因此它间歇性失败的原因。一个解决方法是在这一行将 > 更改为 >=:

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

原因不直观,除非您了解每次调用 DateTime.UtcNow 不一定每次调用都返回一个新值。

尽管 DateTime.UtcNow 精确到 100 纳秒,但它的精度与准确度不同。它依赖于机器的定时器间隔,范围为 1-15 毫秒,但通常设置为 15.25 毫秒,除非您正在处理多媒体。

您可以通过这个 dotnetfiddle 看到它的实际效果.除非您打开了一个将计时器设置为不同值(例如 1 毫秒)的程序,否则您会注意到滴答声之间的差异约为 150000 滴答声,大约 15 毫秒,或正常的系统计时器间隔。

我们还可以通过将对 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 是相等的。