我正在尝试控制对对象的访问,以便在给定的时间跨度内只能访问该对象一定次数。在我所拥有的一个单元测试中,访问限制为每秒一次。所以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;
}
}
}
在我的计算机上运行时,在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));
}
我想知道测试中的循环是否正在以一种单独的线程(或类似的东西)运行循环的每次迭代的方式进行优化,这意味着测试完成的速度比它应该更快,因为Thread.Sleep只有阻止调用它的线程?
答案 0 :(得分:3)
你的问题在Block
方法内部,现在我看一下评论,看来Henk Holterman已经提出了这个问题。
只有_lastReset.Add(_rateLimitSpan)
和DateTime.UtcNow
相等时才会失败。这种情况不会经常发生,因此间歇性地失败的原因。修复方法是在此行中将>
更改为>=
:
if (DateTime.UtcNow > _lastReset.Add(_rateLimitSpan))
这不是直观的原因,除非您了解每次调用DateTime.UtcNow
时不一定每次调用都会返回一个新值。
即使DateTime.UtcNow
精确到 100纳秒,其精度也与精度不同。它依赖于机器的定时器间隔,范围从1到15ms,但通常设置为15.25ms,除非您正在使用多媒体。
您可以使用此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
均等。