Подтвердить что ты не робот

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Ответ 4

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

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

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

Ответ 5

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

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

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

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

Ответ 6

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

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

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

myvar++;

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

Ответ 7

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

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

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

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

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

Ответ 11

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

См. MSDN: InterlockedCompareExchange

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

Ответ 12

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

Прочитайте, что такое volatile, прежде чем вы вслепую начинаете его использовать: http://msdn.microsoft.com/en-us/library/12a04hfd(VS.80).aspx