Потоковое безопасное обновление DateTime с использованием блокировки. *

Можно ли использовать метод синхронизации Interlocked. * для обновления переменной DateTime?

Я хочу сохранить штамп времени последнего касания в памяти. Несколько потоков HTTP будут обновлять последнюю переменную DateTime.

Я ценю, что переменные DateTime представляют собой типы значений, которые заменяются, а не обновляются.

Лучшее, что я могу придумать, - это удерживать отметку времени как общее количество тиков в длинном

class x
{
  long _lastHit;

  void Touch()
  {
    Interlocked.Exchange( ref _lastHit, DateTime.Now.Ticks );   
  }
}

Ответ 1

Да, вы можете это сделать. Самая большая проблема может заключаться в том, что DateTime.Ticks имеет разрешение ~ 20 мс. Поэтому не имеет значения, если вы сохраните переменную DateTime last или long ticks. Но поскольку нет перегрузки Exchange for DateTime, вам нужно использовать long.

Ответ 2

Вариант 1: используйте long с Interlocked и DateTime.ToBinary(). Это не нужно volatile (на самом деле вы получите предупреждение, если бы у вас было это), потому что Interlocked уже обеспечивает атомное обновление. Таким образом вы получаете точное значение DateTime.

long _lastHit;

void Touch()
{
    Interlocked.Exchange(ref _lastHit, DateTime.Now.ToBinary());
}

Чтобы прочитать это атомарно:

DateTime GetLastHit()
{
    long lastHit = Interlocked.CompareExchange(ref _lastHit, 0, 0);
    return DateTime.FromBinary(lastHit);
}

Это возвращает значение _lastHit, и если оно было 0, оно свопило его с 0 (то есть ничего не делает, кроме как читать значение атомарно).

Простое чтение не является хорошим - как минимум, потому что переменная не помечена как изменчивая, поэтому последующие чтения могут просто повторно использовать кешированное значение. Сочетание volatile и Interlocked могло бы работать здесь (я не совсем уверен, но я думаю, что взаимосвязанная запись не может быть замечена в несогласованном состоянии даже другим ядром, выполняющим неблокированное чтение). Но если вы это сделаете, вы получите предупреждение и запах кода для объединения двух разных методов.

Вариант 2: используйте блокировку. Менее желательно в этой ситуации, поскольку в этом случае метод блокировки более эффективен. Но вы можете сохранить правильный тип, и он будет немного яснее:

DateTime _lastHit;
object _lock = new object();

void Touch()
{
    lock (_lock)
        _lastHit = DateTime.Now;
}

Вы можете использовать блокировку, чтобы прочитать это значение! Кстати, помимо взаимного исключения блокировка также гарантирует, что кэшированные значения не могут быть видны, и чтение/запись не могут быть переупорядочены.

Не вариант: ничего не делать (просто напишите значение), отметите ли вы его как volatile или нет. Это неправильно - даже если вы никогда не читаете значение, ваши записи на 32-битной машине могут чередоваться с таким неудачным способом, что вы получаете поврежденное значение:

Thread1: writes dword 1 of value 1
Thread2: writes dword 1 of value 2
Thread2: writes dword 2 of value 2
Thread1: writes dword 2 of value 1

Result: dword 1 is for value 2, while dword 2 is for value 1

Ответ 3

EDIT: на основе комментариев ниже от @romkyns [Спасибо]

Если ваш код запущен на 32-битной машине. то 64-битная длина будет записана в память в двух атомных операциях, которые может прерываться переключателем контекста. Поэтому в целом вам нужно решить эту проблему.

Но для того, чтобы быть понятным, для этого конкретного сценария (написания длинного значения, которое представляет отметки времени), можно утверждать, что проблема настолько неослабна, что не стоит иметь дело с... поскольку (за исключением разделения второй раз каждые 2 ^ 32 тика), значение в высоком слове (32 бита) будет одинаковым для любых двух одновременных записей в любом случае... и даже в очень маловероятном случае есть две параллельные записи, которые охватывают эту границу, которые одновременно прерывают друг друга, и вы получаете привет-слово от одного и низкого слова от другого, если вы также не читаете это значение каждые миллисекунды, следующая запись все равно решит проблему, и никакого вреда не будет сделано. Однако, принимая такой подход, независимо от того, насколько маловероятен плохой случай, он по-прежнему допускает крайне тонкий, но возможный сценарий получения неправильного значения там, где раз в каждые 4 миллиарда тиков... (И удачи, пытаясь воспроизвести эту ошибку...)

Если вы работаете на 64-битной машине, otoh (гораздо более вероятно на данный момент времени, но не гарантируется), тогда значение в слоте 64-битной памяти записывается атомарно, и вам не нужно беспокоиться о concurrency здесь. Условие гонки (это то, что вы пытаетесь предотвратить) может произойти только в том случае, если в некотором блоке обработки, который может быть прерван другим потоком, существует некоторая программная инвариантная, которая находится в недопустимом состоянии. Если все, что вы делаете, записывает в переменную DateTime lastTouch (ячейку памяти), тогда нет такого invlaid-инварианта, который должен быть связан, и поэтому вам не нужно беспокоиться о параллельном доступе.

Ответ 4

Мой подход не один из лучших, но вы можете использовать строку var для хранения отформатированной даты и затем проанализировать ее обратно в datetime:

class x
{
  string _lastHit;

  void Touch()
  {
    Interlocked.Exchange( ref _lastHit, DateTime.Now.ToString("format your date") );   
  }
}

Когда вам нужно использовать это значение, просто проанализируйте DateTime:

DateTime.Parse(_lastHit)

Разбор всегда работает, потому что строка отформатирована с использованием класса DateTime, но вы можете использовать TryParse для обработки возможных ошибок синтаксического анализа