it-roy-ru.com

Безопасно ли читать целочисленную переменную, которая одновременно изменяется без блокировки?

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

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

45
Hongli

атомарное чтение
Как уже было сказано, это зависит от платформы. На x86 значение должно быть выровнено по границе 4 байта. Обычно для большинства платформ чтение должно выполняться в одной инструкции ЦП.

оптимизатор кеширования
Оптимизатор не знает, что вы читаете значение, измененное другим потоком. в этом помогает объявление значения volatile: оптимизатор будет выдавать память для чтения/записи для каждого доступа, вместо того, чтобы пытаться сохранить значение, кэшированное в регистре.

CPU cache
Тем не менее, вы можете прочитать устаревшее значение, так как на современных архитектурах у вас есть несколько ядер с индивидуальным кэшем, который не синхронизируется автоматически. Вам нужен барьер чтения памяти, обычно инструкция для конкретной платформы. 

В Wintel функции синхронизации потоков автоматически добавят полный барьер памяти, или вы можете использовать функции InterlockedXxxx .

MSDN: Проблемы с памятью и синхронизацией , MemoryBarrier Macro

[править] пожалуйста, смотрите также комментарии drhirsch.

33
peterchen

Вы задаете вопрос о чтении переменной, а затем говорите об обновлении переменной, что подразумевает операцию чтения-изменения-записи.

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

Есть несколько (и редких) исключений:

  • Чтение смещено, например, при доступе к 4-байтовому int по нечетному адресу. Обычно вам нужно заставить компилятор со специальными атрибутами сделать некоторое смещение.
  • Размер int больше естественного размера инструкций, например, с использованием 16-битных int в 8-битной архитектуре.
  • Некоторые архитектуры имеют искусственно ограниченную ширину шины. Я знаю только очень старые и устаревшие, такие как 386sx или 68008.
14
Gunther Piez

Я бы рекомендовал не полагаться на какой-либо компилятор или архитектуру в этом случае.
Когда у вас есть смесь читателей и писателей (в отличие от просто читателей или просто писателей), вам лучше синхронизировать их всех. Представьте, что ваш код работает на искусственном сердце кого-то, вы на самом деле не хотите, чтобы он читал неправильные значения, и, конечно же, вы не хотите, чтобы электростанция в вашем городе работала как «бум», потому что кто-то решил не использовать этот мьютекс. Спаси себя ночным сном в долгосрочной перспективе, синхронизируй их.
Если у вас есть только один поток чтения - вы можете использовать только один мьютекс, однако, если вы планируете использовать несколько читателей и несколько писателей, вам потребуется сложный кусок кода для синхронизации. Хорошая реализация блокировки чтения/записи, которая также была бы «честной», еще не видна мной.

8
Dmitry

Представьте, что вы читаете переменную в одном потоке, этот поток прерывается во время чтения, а переменная изменяется записывающим потоком. Теперь, каково значение целого числа чтения после возобновления потока чтения?

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

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

5
NomeN

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

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

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

Для переменных, отличных от Word, чтение и запись обычно не атомарны, и их необходимо синхронизировать другими способами.

3
jalf

Если вы не используете превалирующее значение этой переменной при записи new, то:

Вы можете читать и записывать целочисленные переменные без использования мьютекса. Это потому, что целое число является базовым типом в 32-битной архитектуре, и каждая модификация/чтение значения выполняется с помощью одной операции.

Но, если вы сделаете что-то, например, приращение:

myvar++;

Затем вам нужно использовать mutex, потому что эта конструкция расширена до myvar = myvar + 1, и между чтением myvar и приращением myvar можно изменить myvar. В этом случае вы получите плохую ценность.

2
Vitaly Dyatlov

Хотя, вероятно, было бы безопасно читать целые числа в 32-битных системах без синхронизации. Я бы не стал рисковать. Хотя множественные одновременные чтения не являются проблемой, я не люблю, чтобы записи происходили одновременно с чтением. 

Я бы порекомендовал также разместить чтения в критическом разделе, а затем провести стресс-тестирование приложения на нескольких ядрах, чтобы выяснить, не вызывает ли это слишком много конфликтов. Поиск ошибок параллелизма - кошмар, которого я предпочитаю избегать. Что произойдет, если в будущем кто-нибудь решит изменить int на long long или double, чтобы они могли хранить большие числа?

Если у вас есть библиотека потоков Nice, такая как boost.thread или zthread, то вы должны иметь блокировки чтения/записи. Это было бы идеально для вашей ситуации, поскольку они позволяют многократное чтение, защищая записи.

2
iain

Это может происходить в 8-битных системах, которые используют 16-битные целые числа.

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

1
starblue

В общем, каждая машинная инструкция проходит несколько этапов аппаратного обеспечения при выполнении. Поскольку большинство современных процессоров являются многоядерными или гиперпоточными, это означает, что чтение переменной может начать ее перемещение по конвейеру инструкций, но это не мешает другому ядру ЦП или гиперпотоку одновременно выполнять команду сохранения для того же самого адрес. Две одновременно выполняемые инструкции, read и store, могут «пересекаться», что означает, что чтение получит старое значение непосредственно перед сохранением нового значения.
Для возобновления: вам нужен мьютекс как для чтения, так и для записи.

0
harrymc

Как чтение, так и запись в переменные с параллелизмом должны быть защищены критическим разделом (не мьютексом). Если вы не хотите тратить весь свой день на отладку.

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

0
SadSido