2009-08-29 3 views
42

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

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

+1

Dupe of http://stackoverflow.com/questions/1087771/do-i-need-to-syncronize-thread-access-to-an-int среди многих других – 2009-08-29 10:02:57

+0

Я не думаю, что это конкретный дубликат этого, но он был задан раньше, я думаю, –

+1

Увы, вы правы - было так много на выбор, я думаю, я запутался. – 2009-08-29 10:11:15

ответ

32

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

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

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

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

MSDN: Memory and Synchronization issues, MemoryBarrier Макро

[править] Также см комментарии drhirsch в.

+8

+1 из-за барьера памяти/упоминания о кеше ЦП, а также кэширования оптимизаторов, о чем никто, кажется, не признает. – paercebal

+0

Не совсем ответ, который я ищу, но у вас есть лучший ответ. – Hongli

+8

-1 для того, чтобы быть полностью неправильным в вопросе кэша CPU. Google по протоколу MESI. Пункты памяти предназначены для слабых инструкций по загрузке/хранению заказов (на x86 обычно потоковая передача mmx), что является совершенно другой темой. – hirschhornsalz

1

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

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

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

myvar++; 

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

6

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

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

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

+0

Чтение не требует, чтобы один тактовый цикл был атомарным. Ваша последняя часть неясна для меня - я думаю, вы имеете в виду, что «чтение может быть domne одновременно, но написание записи, чтение или запись других потоков должны быть заблокированы». – peterchen

0

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

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

-3

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

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

+0

volatile не означает «атомный». Это компилятор, зависящий от того, что волатильно на самом деле означает, сегодня большинство компиляторов трактуют его как «не делайте умных трюков оптимизации этой переменной». – nos

+0

В стандарте указано «volatile», что означает, что переменная может измениться или получить доступ из управления потоком, поэтому он никогда не должен кэшироваться в регистре или аналогично. Измененная переменная volatile всегда будет считываться и записываться непосредственно в основную память. Однако он ничего не говорит о синхронизации. – jalf

+0

@jalf: Я считаю, что переменная с надписью «volatile» должна быть прочитана ровно один раз в любое время, когда код C указывает на чтение, даже если было бы более эффективно читать переменную нуль раз или больше раз; и записывается ровно один раз в любой момент, когда код указывает на запись, даже если было бы более эффективно писать его больше (например, a =! b; может быть наиболее эффективным как «a = 0; if (! b) a = 1;» но это не будет разрешено, если a volatile.Это все еще текущее значение? – supercat

0

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

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

-1

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

См MSDN: InterlockedCompareExchange

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

+0

это не отвечает на вопрос OP – NomeN

14

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

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

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

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

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

+1

Нет. Читайте про протоков MESI. Когерентность кеша для обычных операций чтения/записи всегда гарантирована и прозрачна для программного обеспечения. – hirschhornsalz

+0

@ drhirsch: Вы абсолютно уверены, что нет процессора, который требует явного контроля кеша в машинный код? я довольно уверен, что я читал о таких. –

2

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

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

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

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

0

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

1

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

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

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

8

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

+1

Это был технический вопрос; ему нужен технический ответ. – EML

Смежные вопросы