Я понимаю, что std::atomic<>
это атомный объект. Но в какой степени? Насколько я понимаю, операция может быть атомарной. Что именно означает сделать объект атомарным? Например, если два потока одновременно выполняют следующий код:
a = a + 12;
Тогда вся операция (скажем add_twelve_to(int)
) атомная? Или внесены изменения в переменную atomic (так operator=()
)?
c++
multithreading
c++11
atomic
curiousguy
источник
источник
a.fetch_add(12)
если вы хотите атомную RMW.std::atomic
позволяет стандартной библиотеке решать, что нужно для достижения атомарности.std::atomic<T>
это тип, который допускает атомарные операции. Это волшебным образом не делает вашу жизнь лучше, вы все равно должны знать, что вы хотите с ней делать. Это для очень конкретного случая использования, и использование атомарных операций (над объектом), как правило, очень тонкое и должно рассматриваться с нелокальной точки зрения. Так что, если вы уже не знаете, что и зачем вам нужны атомарные операции, тип, вероятно, не очень полезен для вас.Ответы:
Каждое создание и полная специализация std :: atomic <> представляет тип, с которым разные потоки могут одновременно работать (свои экземпляры), не вызывая неопределенного поведения:
std::atomic<>
оборачивает операции, которые в pre-C ++ 11 раз приходилось выполнять с использованием (например) взаимосвязанных функций с MSVC или атомарными bultins в случае GCC.Кроме того,
std::atomic<>
дает вам больше контроля, позволяя различные порядки памяти, которые задают ограничения синхронизации и упорядочения. Если вы хотите узнать больше об атомарности C ++ 11 и модели памяти, эти ссылки могут быть полезны:Обратите внимание, что для типичных случаев использования вы, вероятно, будете использовать перегруженные арифметические операторы или другой их набор :
Поскольку синтаксис оператора не позволяет указывать порядок памяти, эти операции будут выполняться с
std::memory_order_seq_cst
, так как это порядок по умолчанию для всех элементарных операций в C ++ 11. Он гарантирует последовательную согласованность (общий глобальный порядок) между всеми атомарными операциями.Однако в некоторых случаях это может не потребоваться (и ничего не бесплатно), поэтому вы можете использовать более явную форму:
Теперь ваш пример:
не будет оцениваться до одной атомарной операции: она приведет к
a.load()
(который является самой атомарной), затем сложению между этим значением12
иa.store()
(и атомарным) окончательного результата. Как я отмечал ранее, здесьstd::memory_order_seq_cst
будет использоваться.Однако, если вы напишите
a += 12
, это будет атомарная операция (как я уже отмечал ранее) и примерно эквивалентнаa.fetch_add(12, std::memory_order_seq_cst)
.Что касается вашего комментария:
Ваше утверждение верно только для архитектур, которые предоставляют такую гарантию атомности для магазинов и / или грузов. Есть архитектуры, которые этого не делают. Кроме того, обычно требуется, чтобы операции выполнялись с адресом, выровненным по слову / слову, чтобы быть атомарным
std::atomic<>
- это то, что гарантированно будет атомарным на каждой платформе без дополнительных требований. Более того, он позволяет писать код так:Обратите внимание, что условие утверждения всегда будет истинным (и, следовательно, никогда не сработает), поэтому вы всегда можете быть уверены, что данные готовы после
while
выхода из цикла. Это потому:store()
флаг установлен после того,sharedData
как установлен (мы предполагаем, чтоgenerateData()
всегда возвращает что-то полезное, в частности, никогда не возвращаетNULL
) и используетstd::memory_order_release
порядок:sharedData
используется послеwhile
выхода из цикла, и, таким образом,load()
флаг from возвращает ненулевое значение.load()
используетstd::memory_order_acquire
порядок:Это дает вам точный контроль над синхронизацией и позволяет вам явно указать, как ваш код может / не может / не будет / не будет себя вести. Это было бы невозможно, если бы только гарантией была сама атомность. Особенно, когда речь идет об очень интересных моделях синхронизации, таких как порядок выпуска-потребления .
источник
int
s?std::atomic
(std::memory_order
), служат именно той цели, которая ограничивает повторные заказы, которые допускаются.Это вопрос перспективы ... вы не можете применить его к произвольным объектам и сделать их операции атомарными, но можно использовать предоставленные специализации для (большинства) целочисленных типов и указателей.
std::atomic<>
не (использовать шаблонные выражения для) упрощает это до одной атомарной операции, вместо этогоoperator T() const volatile noexcept
элемент делает атомарноеload()
изa
, затем добавляется двенадцать, иoperator=(T t) noexcept
делает astore(t)
.источник
int
не гарантирует переносимость изменений из других потоков, а также их чтение не гарантирует, что вы увидите изменения других потоков, а некоторые вещи, такие какmy_int += 3
, не гарантированно будут выполняться атомарно, если вы не используетеstd::atomic<>
- они могут включать выборка, затем добавление, затем сохранение последовательности, в которой какой-то другой поток, пытающийся обновить то же самое значение, может прийти после выборки и перед сохранением, и засорять обновление вашего потока.std::atomic
существует, потому что многие ISA имеют прямую аппаратную поддержку для негоТо, о чем говорит стандарт C ++
std::atomic
, было проанализировано в других ответах.Итак, теперь давайте посмотрим, что
std::atomic
компилируется, чтобы получить другой вид понимания.Основным выводом этого эксперимента является то, что современные процессоры имеют прямую поддержку целочисленных атомарных операций, например префикс LOCK в x86, и в
std::atomic
основном существуют как переносимый интерфейс для этих операций: что означает инструкция «lock» в сборке x86? В aarch64 будет использоваться LDADD .Эта поддержка допускает более быстрые альтернативы более общим методам, таким как
std::mutex
, которые могут сделать более сложные многокомпонентные разделы атомарными, за счет того, что они медленнее, чемstd::atomic
потому, чтоstd::mutex
это делаетfutex
системные вызовы в Linux, что намного медленнее, чем пользовательские инструкции, создаваемыеstd::atomic
, см. также: создает ли std :: mutex забор?Давайте рассмотрим следующую многопоточную программу, которая увеличивает глобальную переменную в нескольких потоках с различными механизмами синхронизации в зависимости от того, какой препроцессор определен.
main.cpp
GitHub вверх по течению .
Скомпилируйте, запустите и разберите:
Чрезвычайно вероятный «неправильный» вывод состояния гонки для
main_fail.out
:и детерминированный «правильный» вывод остальных:
Разборка
main_fail.out
:Разборка
main_std_atomic.out
:Разборка
main_lock.out
:Выводы:
неатомарная версия сохраняет глобальное в регистр и увеличивает регистр.
Поэтому, в конце, очень вероятно, что четыре записи произойдут обратно в глобальную с одинаковым «неправильным» значением
100000
.std::atomic
компилируется вlock addq
. Префикс LOCK выполняет следующуюinc
выборку, модификацию и обновление памяти атомарно.наш явный встроенный префикс LOCK сборки компилируется почти так же, как
std::atomic
, за исключением того, чтоinc
вместо используется нашadd
. Не уверен, почему GCC выбралadd
, учитывая, что наш INC генерировал декодирование на 1 байт меньше.ARMv8 может использовать либо LDAXR + STLXR, либо LDADD в новых процессорах: как запустить потоки в простом C?
Протестировано в Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51.
источник