我试图控制对一个对象的访问,以便在给定的时间范围内只能对其进行一定次数的访问。在我拥有的一个单元测试中,访问限制为每秒一次。因此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/