Выполняет ли одна инструкция ассемблера атомарно?
Сегодня я столкнулся с этим вопросом:
у вас есть код
static int counter = 0;
void worker() {
for (int i = 1; i <= 10; i++)
counter++;
}
Если worker
будет вызываться из двух разных потоков, какое значение будет counter
после завершения обоих из них?
Я знаю, что на самом деле это может быть что угодно. Но мои внутренние мужества говорят мне, что counter++
скорее всего будет переведен в одну инструкцию ассемблера, и если оба потока выполняются на одном ядре, counter
будет равен 20.
Но что, если эти потоки выполняются на разных ядрах или процессорах, может ли быть состояние гонки в их микрокоде? Можно ли рассматривать одну инструкцию ассемблера как атомную операцию?
Ответы
Ответ 1
В частности, для x86, и в отношении вашего примера: counter++
, существует несколько способов его компиляции. Самый тривиальный пример:
inc counter
Это означает следующие микрооперации:
- загрузить
counter
в скрытый регистр на CPU
- увеличить регистр
- сохранить обновленный регистр в
counter
Это по существу то же самое, что:
mov eax, counter
inc eax
mov counter, eax
Обратите внимание, что если какой-либо другой агент обновляет counter
между загрузкой и хранилищем, он не будет отображаться в counter
после магазина. Этот агент может быть другим потоком в одном ядре, другом ядре в одном CPU, другом процессоре в той же системе или даже внешнем агенте, использующем DMA (Direct Memory Access).
Если вы хотите гарантировать, что этот inc
является атомарным, используйте префикс lock
:
lock inc counter
lock
гарантирует, что никто не сможет обновить counter
между загрузкой и хранилищем.
Что касается более сложных инструкций, вы обычно не можете предположить, что они будут выполняться атомарно, если только они не поддерживают префикс lock
.
Ответ 2
Не всегда - на некоторых архитектурах одна инструкция сборки преобразуется в одну инструкцию машинного кода, а на других - нет.
Кроме того, вы можете никогда предположить, что используемый вами язык программы составляет, казалось бы, простую строку кода в одну инструкцию сборки. Более того, на некоторых архитектурах вы не можете предположить, что один машинный код будет выполняться атомарно.
Вместо этого используйте соответствующие методы синхронизации, в зависимости от языка, в котором вы кодируете.
Ответ 3
Ответ: это зависит!
Вот некоторая путаница вокруг, что такое инструкция ассемблера. Обычно одна инструкция ассемблера преобразуется в ровно одну машинную команду. Исключение происходит, когда вы используете макросы, но вы должны знать об этом.
Тем не менее, вопрос сводится к одной машинной инструкции атома?
В старые добрые времена. Но сегодня, со сложными процессорами, длительными инструкциями, гиперпотоками,... это не так. Некоторые процессоры гарантируют, что некоторые инструкции приращения/уменьшения являются атомарными. Причина в том, что они являются аккуратными для очень простого syncronizing.
Также некоторые команды CPU не так проблематичны. Когда у вас есть простая выборка (из одной части данных, которую процессор может извлечь в один кусок) - сама выборка является атомарной, потому что ее вообще не нужно разделять. Но когда у вас есть несогласованные данные, он снова становится сложным.
Ответ: Это зависит. Внимательно прочитайте инструкцию по эксплуатации устройства. В сомнении, это не так!
Изменить:
О, я видел это сейчас, вы также просите счетчика ++. Заявление, "скорее всего, будет переведено", не может быть доверено вообще. Это во многом также зависит от компилятора! Это становится сложнее, когда компилятор делает различные оптимизации.
Ответ 4
- Операции Increment/Decment для 32-битных или менее целых переменных на одном 32-битном процессоре без технологии Hyper-Threading являются атомарными.
- На процессоре с технологией Hyper-Threading или в многопроцессорной системе операции инкремента/декремента НЕ гарантированно выполняются атомарно.
Ответ 5
Недопустимый комментарий Nathan:
Если я правильно помню свой ассемблер Intel x86, инструкция INC работает только для регистров и не работает непосредственно в ячейках памяти.
Таким образом, счетчик ++ не будет одной инструкцией в ассемблере (просто игнорируя часть после инкремента). Это будет как минимум три инструкции: переменная счетчика нагрузки для регистрации, регистр инкремента, регистр нагрузки обратно к счетчику. И это только для архитектуры x86.
Короче говоря, не полагайтесь на то, что он является атомарным, если он не указан спецификацией языка и что используемый вами компилятор поддерживает спецификации.
Ответ 6
Нет, вы не можете этого допустить. Если это четко не указано в спецификации компилятора. И, кроме того, никто не может гарантировать, что одна инструкция ассемблера действительно атомарна. На практике каждая инструкция ассемблера переводится на число операций микрокода - uops.
Также вопрос о состоянии гонки тесно связан с моделью памяти (когерентность, последовательная, слаженность выпуска и т.д.), для каждого ответа и результата могут быть разные.
Ответ 7
Другая проблема заключается в том, что если вы не объявляете переменную как volatile, генерируемый код, вероятно, не обновлял бы память на каждой итерации цикла, только в конце цикла память обновлялась.
Ответ 8
Не может быть фактический ответ на ваш вопрос, но (если это С# или другой язык .NET), если вы хотите, чтобы counter++
действительно был многопоточным атомом, вы могли бы использовать System.Threading.Interlocked.Increment(counter)
.
См. другие ответы для получения фактической информации о разных способах, почему/как counter++
не может быть атомарным.; -)
Ответ 9
В большинстве случаев нет. Фактически, на x86 вы можете выполнить команду
push [address]
который в C будет выглядеть примерно так:
*stack-- = *address;
Выполняет две передачи памяти в одной команде.
Это практически невозможно сделать за 1 такт, не в последнюю очередь потому, что одна передача памяти также невозможна за один цикл!
Ответ 10
На многих других процессорах различие между системой памяти и процессором больше. (часто этот процессор может быть небольшим или большим, в зависимости от системы памяти, такой как ARM и PowerPC), это также имеет последствия для атомного поведения, если система памяти может изменять порядок чтения и записи.
Для этой цели существуют барьеры памяти (http://en.wikipedia.org/wiki/Memory_barrier)
Короче говоря, в то время как для Intel (с соответствующими префиксами блокировки) достаточно атомарных инструкций, больше нужно делать на неинтеллектуальных операциях, поскольку ввод/вывод памяти может быть не в том же порядке.
Это известная проблема при переносе "незакрепленных" решений от Intel к другим архитектурам.
(Обратите внимание, что многопроцессорные (а не многоядерные) системы на x86 также, по-видимому, нуждаются в барьерах памяти, по крайней мере в 64-битном режиме.
Ответ 11
Я думаю, что вы получите условие гонки на доступ.
Если вы хотите обеспечить атомную операцию в инкрементальном счетчике, вам нужно будет использовать счетчик ++.