Как правильно прочитать поле Interlocked.Increment' int?

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

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

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

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

Ответ 1

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

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

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

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

Ответ 2

Немного мета-проблемы, но хороший аспект об использовании 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, либо любая из данных конструкций должна использоваться.

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

Ответ 3

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

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


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

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

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

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

Ответ 4

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