我试图控制对一个对象的访问,以便在给定的时间范围内只能对其进行一定次数的访问。在我拥有的一个单元测试中,访问限制为每秒一次。因此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仅阻塞线程它正在被调用吗?

最佳答案

您的问题在Block方法内部,现在看一下注释,似乎Henk 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打印为相等。

关于c# - C#编译器优化循环?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/27071516/

10-09 02:26