Будут ли атомарные операции блокировать другие потоки?
Я пытаюсь придумать концепцию "атомное против неатомного". Моя первая проблема в том, что я не смог найти "реальной аналогии" по этому вопросу. Например, отношения между клиентом и рестораном из-за атомных операций или что-то подобное.
Также я хотел бы узнать о том, как атомарные операции помещаются в поточно-ориентированное программирование.
В этом блоге; http://preshing.com/20130618/atomic-vs-non-atomic-operations/ упоминается как:
Операция, действующая на разделяемую память, является атомарной, если она завершается за один шаг относительно других потоков. Когда атомарное хранилище выполняется для совместно используемой переменной, никакой другой поток не может наблюдать модификацию, наполовину завершенную. Когда атомная загрузка выполняется для совместно используемой переменной, она считывает все значение так, как оно появилось в один момент времени. Неатомные грузы и магазины не дают таких гарантий.
Что означает "никакой другой поток не может наблюдать модификацию наполовину завершенную"?
Это означает, что поток будет ждать, пока не будет выполнена атомарная операция? Как этот поток узнал об этой операции атомарной? Например, в.NET я могу понять, если вы блокируете объект, вы устанавливаете флаг, чтобы блокировать другие потоки. Но как насчет атомной? Как другие потоки знают разницу между атомарными и неатомарными операциями?
Также, если приведенное выше утверждение верно, все атомарные операции являются потокобезопасными?
5 ответов
Давайте немного уточним, что такое атомные и что такое блоки. Атомарность означает, что операция либо выполняется полностью, и все ее побочные эффекты видны, либо не выполняется вообще. Таким образом, все остальные потоки могут видеть состояние до операции или после нее. Блок кода, защищенный мьютексом, тоже атомарный, мы просто не называем это операцией. Атомарные операции - это специальные инструкции процессора, которые концептуально похожи на обычные операции, защищаемые мьютексом (вы знаете, что такое мьютекс, поэтому я буду его использовать, несмотря на тот факт, что он реализован с использованием атомарных операций). Процессор имеет ограниченный набор операций, которые он может выполнять атомарно, но благодаря аппаратной поддержке они очень быстрые.
Когда мы обсуждаем блоки потоков, мы обычно вовлекаем мьютексы в диалог, потому что код, охраняемый ими, может выполняться довольно долго. Итак, мы говорим, что поток ожидает мьютекс. Для атомарных операций ситуация такая же, но они быстрые, и мы обычно не заботимся о задержках здесь, так что вряд ли можно услышать слова "блок" и "атомная операция" вместе.
Это означает, что поток будет ждать, пока не будет выполнена атомарная операция?
Да, это будет ждать. CPU ограничит доступ к блоку памяти, в котором находится переменная, а другие ядра CPU будут ждать. Обратите внимание, что по соображениям производительности блоки хранятся только между атомарными операциями. Ядра процессора могут кэшировать переменные для чтения.
Как этот поток узнал об этой операции атомарной?
Используются специальные инструкции процессора. В вашей программе просто написано, что конкретная операция должна выполняться атомарно.
Дополнительная информация:
Есть более сложные части с атомными операциями. Например, на современных процессорах обычно все чтения и записи примитивных типов являются атомарными. Но CPU и компилятору разрешено их переупорядочивать. Таким образом, возможно, что вы изменили некоторую структуру, установили флаг, сообщающий, что она изменилась, но CPU переупорядочивает запись и устанавливает флаг, прежде чем структура фактически будет зафиксирована в памяти. При использовании атомарных операций обычно предпринимаются дополнительные усилия для предотвращения нежелательного переупорядочения. Если вы хотите узнать больше, вы должны прочитать о барьерах памяти.
Простые атомарные магазины и записи не так полезны. Чтобы максимально использовать атомарные операции, вам нужно нечто более сложное. Наиболее распространенным является CAS - сравните и поменяйте местами. Вы сравниваете переменную со значением и меняете ее, только если сравнение прошло успешно.
На типичных современных процессорах атомарные операции выполняются атомарно следующим образом:
Когда выдается инструкция, которая обращается к памяти, логика ядра пытается перевести кэш ядра в правильное состояние для доступа к этой памяти. Обычно это состояние достигается до того, как должен произойти доступ к памяти, поэтому задержка отсутствует.
В то время как другое ядро выполняет атомарную операцию над частью памяти, оно блокирует эту память в своем собственном кэше. Это препятствует тому, чтобы любое другое ядро получило право доступа к этой памяти, пока атомарная операция не завершится.
Если два ядра не выполняют доступ ко многим из одинаковых областей памяти и многие из этих обращений являются записями, это, как правило, вообще не потребует каких-либо задержек. Это происходит потому, что атомарная операция очень быстрая и обычно ядро заранее знает, к какой памяти ему потребуется доступ.
Итак, скажем, к коду памяти последний раз обращались в ядре 1, а теперь ядро 2 хочет сделать атомарный прирост. Когда логика предварительной выборки ядра видит модификацию этой памяти в потоке команд, она направляет кэш для получения этой памяти. Кэш будет использовать межкорпусную шину, чтобы получить право владения этой областью памяти из кэша ядра 1, и он заблокирует эту область в своем собственном кэше.
В этот момент, если другое ядро попытается прочитать или изменить эту область памяти, оно не сможет получить эту область в своем кэше, пока не будет снята блокировка. Это взаимодействие происходит по шине, которая соединяет кеши, и от того, где именно это происходит, зависит, в каком кеше (ах) была память (если вообще не в кеше, то она должна перейти в основную память).
Блокировка кеша обычно не описывается как блокировка потока и потому, что он очень быстрый, и потому, что ядро обычно может делать другие вещи, пытаясь получить область памяти, которая заблокирована в другом кеше. С точки зрения кода более высокого уровня, реализация атомарных компонентов обычно рассматривается как деталь реализации.
Все атомарные операции дают гарантию, что промежуточный результат не будет виден. Вот что делает их атомарными.
Атомарная операция означает, что система выполняет операцию полностью или не выполняет вообще. Чтение или запись int64 является атомарным (64-битная система и 64-битная CLR), поскольку система считывает / записывает 8 байтов за одну операцию, считыватели не видят половину сохраненного нового значения и половину старого значения. Но будьте осторожны:
long n = 0; // writing 'n' is atomic, 64bits OS & 64bits CLR
long m = n; // reading 'n' is atomic
....// some code
long o = n++; // is not atomic : n = n + 1 is doing a read then a write in 2 separate operations
Чтобы атомарность произошла с n++, вы можете использовать Interlocked API:
long o = Interlocked.Increment(ref n); // other threads are blocked while the atomic operation is running
Быть "атомарным" - это атрибут, который применяется к операции, которая обеспечивается реализацией (вообще говоря, аппаратным обеспечением или компилятором). Для реальной аналогии обратите внимание на системы, требующие транзакций, такие как банковские счета. Перевод с одного счета на другой предполагает снятие средств с одного счета и внесение депозита на другой, но, как правило, они должны выполняться атомарно - нет времени, когда деньги были сняты, но еще не внесены, или наоборот.
Итак, продолжая аналогию по вашему вопросу:
Что означает "никакой другой поток не может наблюдать модификацию наполовину завершенную"?
Это означает, что ни один поток не мог наблюдать за двумя учетными записями в состоянии, когда снятие было произведено с одного счета, но не было внесено на другой счет.
В терминах машины это означает, что атомарное чтение значения в одном потоке не увидит значение с некоторыми битами до атомарной записи другим потоком и с некоторыми битами после той же операции записи. Различные операции, более сложные, чем простое чтение или запись, также могут быть атомарными: например, "сравнить и поменять" - это обычно реализуемая атомарная операция, которая проверяет значение переменной, сравнивает его со вторым значением и заменяет его другим значение, если сравниваемые значения были равны атомарно - например, если сравнение успешно, другой поток не может записать другое значение между частями операции сравнения и подкачки. Любая запись другим потоком будет выполняться полностью до или полностью после атомарного сравнения и обмена.
Название вашего вопроса:
Будут ли атомарные операции блокировать другие потоки?
В обычном значении "блок" ответ - нет; атомарная операция в одном потоке сама по себе не приведет к остановке выполнения в другом потоке, хотя это может вызвать ситуацию livelock или иным образом помешать прогрессу.
Это означает, что поток будет ждать, пока не будет выполнена атомарная операция?
Концептуально это означает, что им никогда не нужно будет ждать. Операция либо выполнена, либо не выполнена; это никогда не делается наполовину. На практике атомарные операции могут быть реализованы с использованием мьютексов при значительных затратах производительности. Многие (если не большинство) современных процессоров поддерживают различные элементарные примитивы на аппаратном уровне.
Также, если приведенное выше утверждение верно, все атомарные операции являются потокобезопасными?
Если вы составляете атомарные операции, они больше не являются атомарными. То есть я могу выполнить одну атомарную операцию сравнения и замены, а затем другую, и два сравнения и замены будут по отдельности атомарными, но они делятся. Таким образом, вы все еще можете иметь ошибки параллелизма.
Атомарные операции, которые вы описываете, являются инструкциями внутри процессора, и аппаратное обеспечение гарантирует, что чтение не может произойти в ячейке памяти, пока атомарная запись не будет завершена. Это гарантирует, что поток либо считывает значение перед записью, либо значение после операции записи, но ничего между ними нет - нет возможности прочитать половину байтов значения до записи, а другую половину - после записи.
Код, работающий на процессоре, даже не знает об этом блоке, но на самом деле он ничем не отличается от использования lock
заявление, чтобы убедиться, что более сложная операция (состоящая из множества низкоуровневых инструкций) является атомарной.
Одна атомарная операция всегда является поточно-ориентированной - аппаратное обеспечение гарантирует, что эффект от операции является атомарным - она никогда не прервется в середине.
Набор атомарных операций не является атомарным в подавляющем большинстве случаев (я не эксперт, поэтому я не хочу делать однозначное утверждение, но я не могу придумать случай, когда это будет иначе) - это почему блокировка необходима для сложных операций: вся операция может состоять из нескольких атомарных инструкций, но вся эта операция может быть прервана между любыми из этих двух инструкций, создавая возможность для другого потока, видящего полузапеченные результаты. Блокировка гарантирует, что код, работающий с общими данными, не сможет получить доступ к этим данным, пока не завершится другая операция (возможно, через несколько потоковых переключателей).
Некоторые примеры приведены в этом вопросе / ответе, но вы найдете много других с помощью поиска.