Параллельность: атомарная и изменчивая в модели памяти C ++ 11 - PullRequest
51 голосов
/ 11 января 2012

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

Ответы [ 4 ]

81 голосов
/ 12 января 2012

Во-первых, 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) с помощью ряда потоков будет возвращать последовательность значений без дубликатов или пробелов. При отсутствии дополнительных ограничений, все еще нет гарантии, какие потоки будут видеть, какие значения.

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

30 голосов
/ 11 января 2012

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

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

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

3 голосов
/ 24 октября 2014

Volatile и Atomic служат для разных целей.

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

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

3 голосов
/ 02 августа 2014

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

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

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

...