Параллельность: атомарная и энергозависимая в модели памяти C++11

Глобальная переменная распределяется между двумя одновременно запущенными потоками на двух разных ядрах. Потоки записывают и читают переменные. Для атомарной переменной один поток может прочитать устаревшее значение? Каждое ядро ​​может иметь значение общей переменной в своем кэше, и когда один поток записывает в свою копию в кэше, другой поток другого ядра может считывать устаревшие значения из своего собственного кэша. Или компилятор выполняет строгий порядок в памяти для чтения последнего значения из другого кэша? Стандартная библиотека C++11 имеет поддержку std::atomic. Чем это отличается от ключевого слова volatile? Как изменчивые и атомарные типы будут вести себя по-разному в приведенном выше сценарии?

4 ответа

Решение

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

Если у вас есть глобальная переменная, которая разделяется между потоками, например:

std::atomic<int> ai;

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

При отсутствии какой-либо дополнительной синхронизации, если один поток записывает значение в ai тогда нет ничего, что гарантировало бы, что другой поток увидит значение в любой заданный период времени. Стандарт определяет, что он должен быть виден "в разумный период времени", но любой данный доступ может вернуть устаревшее значение.

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

Если у вас есть 2 общие переменные x а также y, изначально ноль, и один поток записать 1 в x а другой пиши 2 в yзатем третий поток, который читает оба, может видеть либо (0,0), (1,0), (0,2) или (1,2), поскольку между операциями нет ограничений на упорядочение, и, таким образом, операции могут появляться в любом порядке в глобальном порядке.

Если обе записи происходят из одного потока, что делает x=1 до y=2 и читающая ветка читает y до x тогда (0,2) больше не является допустимым параметром, так как чтение y==2 подразумевает, что ранее написать x виден Другие 3 пары (0,0), (1,0) и (1,2) все еще возможны, в зависимости от того, как 2 чтения чередуются с 2 записями.

Если вы используете другие заказы памяти, такие как std::memory_order_relaxed или же std::memory_order_acquire затем ограничения еще более ослабляются, и единый глобальный порядок больше не применяется. Потоки даже не обязательно должны согласовывать порядок двух хранилищ для разделения переменных, если нет дополнительной синхронизации.

Единственный способ гарантировать, что у вас есть "последнее" значение, это использовать операцию чтения-изменения-записи, такую ​​как exchange(), compare_exchange_strong() или же fetch_add(), Операции чтения-изменения-записи имеют дополнительное ограничение, заключающееся в том, что они всегда работают с "последним" значением, поэтому последовательность ai.fetch_add(1) Операции с рядом потоков вернут последовательность значений без дубликатов или пробелов. В отсутствие дополнительных ограничений, все еще нет гарантии, какие потоки будут видеть, какие значения.

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

volatile и атомные операции имеют другой фон, и были введены с другим намерением.

volatile восходит к давним временам и в основном предназначен для предотвращения оптимизации компилятора при доступе к памяти, отображаемой в IO. Современные компиляторы, как правило, не более чем подавляют оптимизацию для volatileхотя на некоторых машинах этого недостаточно даже для отображения ввода-вывода в память. За исключением особого случая обработчиков сигналов, и setjmp,longjmp а также getjmp последовательности (где стандарт C, а в случае сигналов, стандарт Posix дает дополнительные гарантии), его следует считать бесполезным на современном компьютере, где без специальных дополнительных инструкций (ограждений или барьеров памяти) аппаратное обеспечение может переупорядочить или даже подавить определенные доступы. Так как вы не должны использовать setjmpи другие. в C++ это более или менее оставляет обработчики сигналов, а в многопоточной среде, по крайней мере, в Unix, есть и лучшие решения для них. И, возможно, отображение ввода-вывода в память, если вы работаете над кодом ядра и можете гарантировать, что компилятор сгенерирует все, что нужно для рассматриваемой платформы. (Согласно стандарту, volatile доступ - это наблюдаемое поведение, которое должен соблюдать компилятор. Но компилятор получает возможность определить, что подразумевается под "доступом", и большинство, похоже, определяет его как "машинная инструкция загрузки или сохранения была выполнена". Что на современном процессоре даже не означает, что на шине обязательно есть цикл чтения или записи, тем более что он находится в том порядке, в котором вы ожидаете.)

Учитывая эту ситуацию, стандарт C++ добавил атомарный доступ, который обеспечивает определенное количество гарантий для всех потоков; в частности, код, сгенерированный вокруг атомарного доступа, будет содержать необходимые дополнительные инструкции, чтобы предотвратить переупорядочивание доступа аппаратными средствами и гарантировать, что доступ распространяется до глобальной памяти, разделяемой между ядрами на многоядерной машине. (В какой-то момент в процессе стандартизации Microsoft предложила добавить эту семантику вvolatileи я думаю, что некоторые из их компиляторов C++ делают. Однако после обсуждения вопросов в комитете общее мнение, включая представителя Microsoft, заключалось в том, что лучше уйти volatile с его оригинальным значением и для определения атомарных типов.) Или просто используйте примитивы системного уровня, такие как мьютексы, которые выполняют любые инструкции, необходимые в их коде. (Они должны. Вы не можете реализовать мьютекс без каких-либо гарантий относительно порядка обращений к памяти.)

Вот краткий обзор двух вещей:

1) Волатильное ключевое слово:
Сообщает компилятору, что это значение может измениться в любой момент, и, следовательно, оно не должно НИКОГДА кешировать его в регистре. Посмотрите на старое ключевое слово "register" в C. "Volatile" - это, по сути, оператор "-", чтобы зарегистрировать "+". Современные компиляторы теперь выполняют оптимизацию, которая "регистрируется", используется для явного запроса по умолчанию, поэтому вы видите только "volatile". Использование квалификатора volatile гарантирует, что ваша обработка никогда не использует устаревшее значение, но не более того.

2) Атомный:
Атомарные операции изменяют данные за один такт, так что ЛЮБОЙ другой поток не может получить доступ к данным в середине такого обновления. Они обычно ограничиваются любыми одночасовыми инструкциями по сборке, которые поддерживает аппаратное обеспечение; такие вещи, как ++,- и обмен 2 указателями. Обратите внимание, что это ничего не говорит об ORDER, разные потоки будут запускать атомарные инструкции, только то, что они никогда не будут выполняться параллельно. Вот почему у вас есть все эти дополнительные опции для навязывания заказа.

Летучие и атомные служат разным целям.

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

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

Другие вопросы по тегам