Как правильно прочесть поле int с блокировкой и инкрементом?

Предположим, у меня есть энергонезависимое поле int и поток, который Interlocked.Incrementсидеть. Может ли другой поток безопасно прочитать это непосредственно, или чтение также должно быть заблокировано?

Ранее я думал, что мне нужно использовать блокированное чтение, чтобы гарантировать, что я вижу текущее значение, так как, в конце концов, поле не является изменчивым. Я использую Interlocked.CompareExchange(int, 0, 0) чтобы достичь этого.

Тем не менее, я наткнулся на этот ответ, который предполагает, что на самом деле обычные чтения всегда будут видеть текущую версию Interlocked.IncrementЗначение ed, и поскольку чтение int уже атомарно, нет необходимости делать что-то особенное. Я также нашел запрос, в котором Microsoft отклоняет запрос Interlocked.Read (ref int), еще раз предполагая, что это полностью избыточно.

Так могу ли я действительно безопасно прочитать самую актуальную ценность такого int поле без Interlocked?

5 ответов

Если вы хотите гарантировать, что другой поток будет читать последнее значение, вы должны использовать Thread.VolatileRead(), (*)

Сама операция чтения является атомарной, так что она не вызовет никаких проблем, но без энергозависимого чтения вы можете получить старое значение из кэша, или компилятор может оптимизировать ваш код и полностью исключить операцию чтения. С точки зрения компилятора достаточно, чтобы код работал в однопоточной среде. Изменчивые операции и барьеры памяти используются для ограничения способности компилятора оптимизировать и переупорядочивать код.

Есть несколько участников, которые могут изменить код: компилятор, JIT-компилятор и процессор. Неважно, какой из них показывает, что ваш код не работает. Единственная важная вещь - это модель памяти.NET, так как она определяет правила, которым должны следовать все участники.

(*) Thread.VolatileRead() на самом деле не получить последнее значение. Это прочитает значение и добавит барьер памяти после чтения. Первое энергозависимое чтение может получить кэшированное значение, но второе получит обновленное значение, потому что барьер памяти первого энергозависимого чтения вызвал обновление кэша, если это было необходимо. На практике эта деталь не имеет большого значения при написании кода.

Немного мета проблемы, но хороший аспект использования Interlocked.CompareExchange(ref value, 0, 0) (игнорируя очевидный недостаток, который труднее понять при использовании для чтения), что он работает независимо от int или же long, Это правда, что int чтения всегда атомарные, но long чтения нет или не может быть, в зависимости от архитектуры. К несчастью, Interlocked.Read(ref value) работает только если value имеет тип long,

Рассмотрим случай, когда вы начинаете с int поле, которое делает невозможным использование Interlocked.Read()так что вместо этого вы будете читать значение напрямую, так как оно в любом случае атомарно. Однако позже в процессе разработки вы или кто-то еще решите, что long требуется - компилятор не предупредит вас, но теперь у вас может быть небольшая ошибка: доступ на чтение больше не гарантируется как атомарный. Я нашел с помощью Interlocked.CompareExchange() лучшая альтернатива здесь; Это может быть медленнее, в зависимости от базовых инструкций процессора, но это безопаснее в долгосрочной перспективе. Я не знаю достаточно о внутренностях Thread.VolatileRead() хоть; Это может быть "лучше" в отношении этого варианта использования, поскольку он обеспечивает еще больше подписей.

Я бы не пытался читать значение напрямую (т.е. без какого-либо из вышеперечисленных механизмов) в цикле или при любом жестком вызове метода, поскольку, даже если записи являются энергозависимыми и / или барьером памяти, ничто не говорит компилятору, что значение поля может фактически меняться между двумя чтениями. Итак, поле должно быть volatile или любая из данных конструкций должна быть использована.

Мои два цента.

Вы правы, что вам не нужна специальная инструкция для атомарного чтения 32-битного целого числа, однако это означает, что вы получите "целое" значение (т.е. вы не получите часть одной записи и часть другой). У вас нет никаких гарантий, что значение не изменится, как только вы его прочитаете.

Именно в этот момент вам нужно решить, нужно ли вам использовать какой-либо другой метод синхронизации для управления доступом, например, используете ли вы это значение для чтения члена из массива и т. Д.


Короче говоря, атомарность гарантирует, что операция происходит полностью и неделимо. Учитывая некоторую операцию A что содержало N шаги, если вы сделали это на операцию сразу после A Вы можете быть уверены, что все N шаги происходили в отрыве от одновременных операций.

Если у вас было два потока, которые выполняли атомарную операцию A вам гарантировано, что вы увидите только полный результат одного из двух потоков. Если вы хотите координировать потоки, атомарные операции могут быть использованы для создания необходимой синхронизации. Но атомарные операции сами по себе не обеспечивают синхронизацию более высокого уровня. Interlocked Семейство методов сделано доступным, чтобы обеспечить некоторые фундаментальные атомарные операции.

Синхронизация - это более широкий вид управления параллелизмом, часто построенный вокруг атомарных операций. Большинство процессоров имеют барьеры памяти, которые позволяют вам гарантировать, что все строки кэша сброшены, и у вас есть единое представление о памяти. Изменчивые чтения - это способ обеспечить постоянный доступ к определенной ячейке памяти.

Хотя это не относится непосредственно к вашей проблеме, чтение ACID (атомарность, согласованность, изоляция и долговечность) в отношении баз данных может помочь вам с терминологией.

Я, будучи параноиком, для целых значений

Да, все, что вы прочитали, правильно. Interlocked.Increment разработан таким образом, что нормальные чтения не будут ложными при внесении изменений в поле. Читать поле не опасно, писать поле - это опасно.

Другие вопросы по тегам