2014-11-21 8 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', и было бы лучше использовать' секундомер' вместо 'DateTime.UtcNow' для отслеживания времени - но я не вижу ничего явно неправильного, что бы объяснить проблему. Единственное, что приходит в голову, это возможность того, что (поскольку вы не используете «Секундомер»), если по какой-то причине системное время не может быть обновлено, часы будут неправильно измерять фактическое прошедшее время. Скорее всего, это не имеет ничего общего с оптимизацией компилятора, в частности, с использованием потоковой передачи. –

+0

Задайте себе, что произойдет, когда '_lastReset.Add (_rateLimitSpan) == DateTime.UtcNow'. Кроме того, для этого хорошо написанного кода любопытно и непрозрачно полагаться на инициализацию '_lastReset = default (DateTime). Кроме того, реальный код не должен 'Sleep()'.Подумайте о том, чтобы ждать Waithandle или что-то в этом роде. –

+0

Неверный, но, безусловно, теоретически возможный способ отказа этого теста - синхронизация с сервером времени происходит во время выполнения вашего теста (и у вас нет контроля над этим). Было бы очень удивительно, если бы это была проблема. – hvd

ответ

3

Ваша проблема в методе Block, и теперь, когда я смотрю на комментарии, похоже, что Хенк Холтерман уже это сделал.

Он будет работать только в том случае, если _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 как равны.

Смежные вопросы