Безопасно ли читать целочисленную переменную, которая одновременно изменяется без блокировки? - PullRequest
46 голосов
/ 29 августа 2009

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

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

Ответы [ 12 ]

34 голосов
/ 29 августа 2009

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

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

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

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

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

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

14 голосов
/ 29 августа 2009

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

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

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

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

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

5 голосов
/ 29 августа 2009

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

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

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

3 голосов
/ 29 августа 2009

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

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

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

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

2 голосов
/ 29 августа 2009

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

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

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

2 голосов
/ 29 августа 2009

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

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

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

myvar++;

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

1 голос
/ 29 августа 2009

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

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

0 голосов
/ 29 августа 2009

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

0 голосов
/ 29 августа 2009

Зависит от вашей платформы. Большинство современных платформ предлагают элементарные операции для целых чисел: Windows имеет InterlockedIncrement, InterlockedDecrement, InterlockedCompareExchange и т. Д. Эти операции обычно поддерживаются базовым оборудованием (читай: ЦП), и они обычно дешевле, чем использование критического раздела или других механизмов синхронизации. 1001 *

См. MSDN: InterlockedCompareExchange

Я считаю, что Linux (и современные варианты Unix) поддерживают аналогичные операции в пакете pthreads, но я не претендую на то, чтобы быть там экспертом.

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