Когда мне действительно нужно использовать atomic‹bool› вместо bool?

Разве atomic<bool> не является избыточным, потому что bool является атомарным по своей природе? Я не думаю, что возможно иметь частично измененное логическое значение. Когда мне действительно нужно использовать atomic<bool> вместо bool?


person Community    schedule 01.05.2013    source источник
comment
Вам нужно atomic<bool>, чтобы избежать условий гонки. Состояние гонки возникает, если два потока обращаются к одной и той же ячейке памяти, и хотя бы один из них является операцией записи. Если ваша программа содержит условия гонки, поведение не определено.   -  person nosid    schedule 01.05.2013
comment
@nosid: Да, но ОП говорит, что он не верит, что у вас может быть частичная операция записи для логического значения, как вы можете, скажем, значение int, где вы копируете каждый байт или слово этого значения по отдельности. Поэтому не должно быть условий гонки, если запись уже является атомарной.   -  person Robert Harvey    schedule 01.05.2013
comment
sizeof(bool) определяется реализацией и предположительно может быть > 1, поэтому в некоторых случаях возможно, что он не является атомарным.   -  person Paul R    schedule 01.05.2013
comment
Связанный: stackoverflow.com/questions/5067492/   -  person Paul R    schedule 01.05.2013
comment
Без atomic нет никакой гарантии, что вы когда-либо увидите обновление в другом потоке или что вы увидите обновления переменных в том же порядке, в котором вы делаете их в другом потоке.   -  person jcoder    schedule 01.05.2013
comment
comment
@jcoder: Чтобы быть очень педантичным, я считаю, что стандарт на самом деле не предписывает когерентность кеша (или, скорее, распространение видимости) - он оставлен как качество реализации с максимальным усилием. То есть у вас может быть атомарная переменная, синхронизирующая два потока, но нет никакой гарантии (в стандарте), что изменение когда-либо распространится. Дело только в том, что если изменение распространяется, то оно передает отношение "произошло до". (Например, поток A может сохранить разблокированное, но поток B может всегда продолжать читать заблокированный. Только если он читает разблокированный, он будет продолжать безопасно.)   -  person Kerrek SB    schedule 01.05.2013
comment
@KerrekSB не для этого ли предназначены функции сохранения и загрузки std::atomic?   -  person jcoder    schedule 01.05.2013
comment
@jcoder: я не уверен. Сохранение и загрузка (с соответствующими порядками!) являются точками синхронизации. Это означает, что если вы загружаете определенное значение, вы знаете, что произошло сохранение этого значения. Но нет никакой гарантии, что вы в конце концов загрузите сохраненное значение. Вы также можете навсегда продолжить загрузку старого значения. (Тем не менее, обмен будет другим и обязательно должен распространяться.)   -  person Kerrek SB    schedule 02.05.2013
comment
Одной из основных причин использования атомарности является подавление оптимизации локального кэширования состояния переменных. Ничто не гарантирует, что глобальная переменная или член класса, установленный в одном потоке, будет виден в другом потоке, который выполняет while (условие)... В этом случае они заменяют плохо определенное ключевое слово volatile точным семантика.   -  person Wheezil    schedule 07.12.2018


Ответы (6)


Ни один тип в C++ не является "атомарным по своей природе", если только он не является std::atomic* чем-то. Это потому, что так сказано в стандарте.

На практике фактические аппаратные инструкции, которые выдаются для управления std::atomic<bool>, могут (а могут и не быть) такими же, как и для обычного bool, но атомарность — это более широкое понятие с более широкими разветвлениями (например, ограничения на переупорядочивание компилятора). Кроме того, некоторые операции (например, отрицание) перегружаются атомарной операцией, чтобы создать на аппаратном уровне инструкции, явно отличающиеся от нативной, неатомарной последовательности чтения-модификации-записи неатомарной переменной.

person Kerrek SB    schedule 01.05.2013
comment
небольшая поправка, std::atomic_flag является единственным исключением, хотя его имя также начинается с атома. - person yngccc; 01.05.2013
comment
@yngccc: Я думаю, именно поэтому Керрек С.Б. написал std::atomic*, а не std::atomic<*>. - person Sebastian Mach; 17.10.2018
comment
этот std::atomic* включает std::atomic‹*› ? - person Nüsrat Nuriyev; 18.02.2020

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

person Dims    schedule 01.05.2013
comment
ключевое слово volatile может решить проблему многопроцессорности? - person Vincent Xue; 29.06.2015
comment
Нет. Volatile не имеет ничего общего с заборами памяти. - person unexpectedvalue; 04.10.2015
comment
Просто для ясности. Комментарий @Vincent, возможно, возник из-за понимания ключевого слова volatile в Java. Ключевое слово volatile в Java управляет ограничениями памяти, но его поведение сильно отличается от ключевого слова volatile в C, которое этого не делает. Этот вопрос объясняет разницу далее. - person Pace; 19.12.2016
comment
Почему атомарность связана с порядком памяти? Подразумевает ли std::atomic‹T› барьеры? Если да, то не идет ли это немного дальше, чем просто атомарность? - person nmr; 18.10.2017
comment
stackoverflow.com/a/14625122/580677 Да, оказывается, std::atomic‹T› делает больше, чем говорит на жести. Конечно, это так. - person nmr; 19.10.2017
comment
Я думаю, что это действительно правильный ответ. Потому что ответ о стандартах bla-bla-bla... sizeof(bool) может быть › 1 — это то, чего никогда не бывает в реальной жизни. Все основные компиляторы имеют sizeof(bool) == 1, и все операции чтения/записи будут работать одинаково для bool и atomic‹bool›. Но многоядерный процессор и пропущенный барьер памяти — это то, что произойдет почти со 100% вероятностью для любого современного приложения и оборудования. - person Ezh; 02.09.2018
comment
@nmr: атомарность связана с упорядочением, поэтому вы можете использовать ее для создания синхронизации между потоками. Если вам это не нужно, используйте std::memory_order_relaxed, чтобы получить атомарность без упорядочения. Этот ответ совершенно неверен: барьеры не создают атомарности, потому что, например, они не предотвращают появление хранилища из другого потока между tmp=var; tmp++; var=tmp;. Чтобы преобразовать эту последовательность в атомарный RMW, необходимы специальные инструкции ЦП. См. также Может ли num++ быть атомарным для 'int num'? - person Peter Cordes; 03.07.2019
comment
@Dims: пожалуйста, удалите этот ответ и прекратите распространять это заблуждение о барьерах и о том, как работает когерентность кеша. Если вы хотите сказать, что atomic по умолчанию использует последовательную согласованность, так и скажите. Это не требуется для атомарности, а конфликтующие значения в кеше для одной и той же переменной невозможны: когерентность кеша MESI предотвращает это. atomic подразумевает некоторые из тех же вещей, что и volatile, поэтому компилятор не поднимает значение переменной в регистр. Это непоследовательно. - person Peter Cordes; 03.07.2019
comment
@PeterCordes Я не говорил, что барьеры подразумевают атомарность, пожалуйста, перечитайте ответ. - person Dims; 04.07.2019
comment
О верно. Но то, что вы сказали, по-прежнему неверно. Барьеры не нужны, чтобы сделать магазин видимым во всем мире. Они нужны вам только в том случае, если вам нужно, чтобы этот поток ждал, пока это не произойдет само по себе. Теоретически у вас может быть неэффективная реализация C++ в системе с некогерентной общей памятью, но обычно вы используете MPI или другую передачу сообщений для связи между доменами когерентности в очень редких огромных кластерах с некоторой общей, но не когерентной памятью. Что atomic<T> действительно делает в обычных системах, так это не позволяет компилятору сохранять значение в закрытом для потока регистре. - person Peter Cordes; 05.07.2019
comment
Барьеры памяти не связаны с многопоточностью - person G Huxley; 01.11.2019

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

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

Но зачем проходить через все это? atomic позаботится об этом за вас и, вероятно, сделает это лучше, чем вы сами.

person Pete Becker    schedule 01.05.2013
comment
Переключение задач не приводит к разрыву, если только для сохранения переменной не потребовалось несколько инструкций. Целые инструкции атомарны. прерывания на одном ядре (они либо полностью завершаются до прерывания, либо любая частичная работа отбрасывается. Это часть того, для чего нужны буферы хранения). Разрыв гораздо более вероятен между потоками на отдельных ядрах, которые фактически выполняются одновременно, потому что тогда да можно сделать разрыв между частями магазина одной инструкцией, например невыровненный магазин или слишком широкий для шины. - person Peter Cordes; 03.07.2019
comment
Нет, ядро ​​не может записать строку кэша, пока оно не станет исключительным владельцем этой строки. Протокол когерентности кэша MESI обеспечивает это. (См. Может ли num++ быть атомарным для 'int num'?). Настоящая проблема для C++ заключается в том, что компилятору разрешено предполагать, что неатомарные переменные не изменяются другими потоками, поэтому он может поднимать нагрузки из циклов и хранить их в регистрах или оптимизировать. например превращая while(!var) {} в if(!var) infloop();. Эта часть atomic похожа на то, что делает volatile: всегда перечитывается из памяти (которая кэшируется, но связна). - person Peter Cordes; 03.07.2019
comment
@PeterCordes — у меня недостаточно мудрости, чтобы делать утверждения о поведении каждой возможной аппаратной архитектуры, на которой может выполняться код C++. Может быть, да, но это не значит, что вы должны воскресить тему шестилетней давности. - person Pete Becker; 03.07.2019
comment
Чтобы свернуть свои собственные атомарные вычисления, вам не нужно сбрасывать кэши; вы бы использовали volatile + барьеры. И вам понадобится встроенный ассемблер для атомарных элементов RMW, таких как var += 1;, чтобы он был одним атомарным приращением вместо атомарной загрузки, приращением внутри ЦП, а затем отдельным атомарным хранилищем. - person Peter Cordes; 03.07.2019
comment
Я упрощал в своем комментарии, чтобы говорить об обычных машинах: конечно, можно иметь реализацию C++ на машине, которая требует явного сброса для согласованности, но модель памяти C++ и концепция релиз-хранилищ эффективны только с когерентной памятью. В противном случае каждому релиз-хранилищу или хранилищу seq-cst пришлось бы сбрасывать все, за исключением умных оптимизаций «как если бы». Все основные SMP-системы когерентны по кэш-памяти. Есть несвязные большие кластеры с общей памятью, но они используют ее для передачи сообщений, а не для запуска потоков одной программы. - person Peter Cordes; 03.07.2019
comment
@PeterCordes — «упрощая разговор об обычных машинах» означает, что ваши комментарии не относятся к значению «атомарности» в стандарте C++, который описывает требования к реализации на любом машина. «На небе и на земле есть больше вещей… чем мы мечтали в вашей философии». - person Pete Becker; 05.07.2019
comment
Ваш ответ представил обсуждение деталей реализации. Есть много возможных ошибок, которые вы можете придумать, если хотите изобрести гипотетическое оборудование. Но да, язык в этом ответе не доходит до того, чтобы подразумевать, что это проблема на обычном оборудовании, в отличие от вашего ответа на может ли логическая операция чтения/записи быть не атомарным на x86? (один из дубликатов этого вопроса, но он помечен как x86 и, следовательно, не может иметь несогласованных кешей между потоками). - person Peter Cordes; 05.07.2019
comment
Эффективная реализация C++ на машине, требующей явной когерентности, звучит маловероятно, поэтому странно, когда хранение значений в регистрах создает ту же проблему, о которой вы говорите, с помощью механизма, который существует на всех реальных процессорах. Что меня раздражает в этом ответе, так это то, что он не помогает прояснить распространенное заблуждение о когерентности кеша в реальных системах, которые мы используем. Многие думают, что на x86 или ARM необходима какая-то явная очистка и что возможно чтение устаревших данных из кеша. - person Peter Cordes; 05.07.2019
comment
Если бы стандарт C++ вообще заботился об эффективности несогласованной разделяемой памяти, выполняющей несколько потоков, были бы такие механизмы, как хранилища релизов, которые делали бы глобально видимым только определенный массив или другой объект, а не каждый другой операции до этой точки (включая все неатомарные операции). В когерентных системах хранилищам релизов нужно просто дождаться завершения и фиксации предыдущих текущих загрузок/сохранений, а не записывать обратно все содержимое каких-либо частных кэшей. Доступ к нашим грязным приватным кешам другими ядрами происходит по запросу. - person Peter Cordes; 05.07.2019
comment
@PeterCordes — этот ответ не предназначался для решения проблемы когерентности кеша в системах, которые использует большинство людей. Предполагалось, что это неуместно, если вы используете C++ atomics, поскольку реализация будет обрабатывать любые проблемы, присутствующие на целевом оборудовании. Но вы, очевидно, невосприимчивы к последствиям написания стандарта, и я не собираюсь больше тратить время, пытаясь вас обучить. - person Pete Becker; 05.07.2019
comment
Я думаю, вы упускаете суть моих комментариев. Я знаю, что стандарт C++ написан аппаратно-независимым способом, и это определенно хорошо. Но этот ответ начинается с того, что атомарные типы C++ имеют дело с тремя потенциальными проблемами, поэтому вы заявляете, что собираетесь охватить каждую возможную деталь оборудования, которая может быть проблемой для C++ atomic, и что их всего 3. ISO C++ даже не упоминает кеши; это на вас, поэтому я думаю, что справедливо критиковать ваш выбор того, о чем говорить, что касается тайников. Вы не технически неправы, просто ИМО вводит в заблуждение. - person Peter Cordes; 05.07.2019
comment
OTOH, вы убедили меня, что нагнетание страха по поводу непоследовательного кэширования на самом деле имеет здесь некоторый смысл: это то, о чем atomic<bool> позаботится за вас, если это проблема в целевой системе. Даже несмотря на то, что это не относится ни к одной реализации C++, о которой я знаю, если читатель не знал об этом, то он определенно не готов сворачивать свои собственные атомарные блоки поверх volatile или барьеров памяти, специфичных для компилятора. - person Peter Cordes; 05.07.2019
comment
std::atomic<bool> дает вам как минимум 2 другие вещи, о которых вы не упомянули: четко определенное поведение, если другой поток изменяет значение, которое вы читаете в цикле. (Таким образом, это имеет то же самое, что и volatile: принудительное повторное чтение из памяти). И сделайте операции чтения-изменения-записи, такие как b ^= 1; atomic. За исключением того, что atomic<bool> не имеет функции инвертирования, но есть b.compare_exchange_weak или .exchange, которые являются атомарными. например на x86 вы получаете lock cmpxchg вместо просто загрузки/ветвления или чего-то еще. Как атомарно отменить std::atomic_bool? - person Peter Cordes; 05.07.2019
comment
@PeterCordes - Как это дает вам четко определенное поведение для чтения volatile в цикле? Насколько я знаю, это распространенное заблуждение: это вызывает актуальное чтение, следовательно, решает проблему энергонезависимого цикла bool, но, насколько я знаю, стандарт здесь не дает никаких гарантий. Трудно понять, как такие гарантии будут написаны в любом случае, поскольку модель в основном касается относительного поведения в стиле «происходит до» и не ссылается на глобальные часы (AFAIK). - person BeeOnRope; 05.07.2019
comment
@BeeOnRope: volatile не дает вам четко определенное поведение для этого в ISO C++. Только в определенных реализациях (таких как GNU C для известного набора ISA) вы можете с пользой накатывать свои собственные атомарные вычисления поверх volatile, игнорируя тот факт, что технически это UB, как и ядро ​​Linux. Я должен был сказать и вместо или вещей, определяемых реализацией. Я думаю, что на практике вам будет трудно найти реализацию, в которой volatile будет ломаться для этого; как я уже сказал, я не думаю, что существуют какие-либо реализации C++ на несогласованном оборудовании с общей памятью, и это очень нестандартно. - person Peter Cordes; 05.07.2019
comment
Извините, @PeterCordes, я говорил о atomic, а не о volatile. Я утверждаю, что atomic не дает вам поведения, при котором один поток, читающий переменную, увидит новое значение после того, как другой поток запишет его, теоретически. На практике это происходит из-за проблемы QoI и потому, что оптимизация, которая сломает это, маловероятна. - person BeeOnRope; 05.07.2019
comment
@BeeOnRope: Я думаю, что это было замыслом стандарта, хотя в некоторых случаях он все же позволял оптимизировать атомарность. Да, оптимизация атомарности — сложная проблема. Но я не думаю, что вы когда-либо сможете оправдать поднятие расслабленной атомарной нагрузки из цикла ожидания вращения в соответствии с любым разумным прочтением правила «как если бы». Любая реальная цель компилятора будет иметь какой-то максимально правдоподобный интервал времени переупорядочивания, и он будет меньше бесконечности. Поэтому предположение, что все бесконечное количество операций чтения (включая первое) произошло до записи, неразумно. - person Peter Cordes; 05.07.2019
comment
Однако язык не написан в этих терминах (подъем, изменение порядка и т. д.). Вам не нужно искать причины, по которым такая оптимизация будет разрешена через as if, потому что базовый вариант этого не гарантирует. Вам нужно искать любой язык, который предполагает, что запись одного потока гарантированно будет замечена другим потоком, всегда. Насколько я знаю, нет. Так что это не вопрос оптимизации, нарушающей что-то, что в противном случае гарантировано стандартом: насколько я знаю, это вообще не гарантируется. @ПитерКордес - person BeeOnRope; 05.07.2019
comment
@BeeOnRope: Но на самом деле рассуждения о возможных порядках должны касаться порядков, разрешенных в абстрактной машине C ++, а затем выбора одного такого порядка во время компиляции. Так что, к сожалению, целевая реальность на самом деле не приходит к этому так рано. - person Peter Cordes; 05.07.2019
comment
@BeeOnRope: Хороший вопрос. Чтобы if(!b) infloop(); было эквивалентно while(!b){} согласно правилу "как если", вам нужно решить, что все бесконечное число операций чтения b являются непрерывными в глобальном порядке операций чтения и записи для b. то есть все они происходят до любой возможной записи из другого потока. Я предполагаю, что теоретически это возможно для реализации DeathStation 9000, но совершенно очевидно, что это не намерение стандарта. Это может даже не соответствовать стандарту в зависимости от порядка запуска программы своих потоков. - person Peter Cordes; 05.07.2019
comment
@BeeOnRope: есть формулировка в сноске/рекомендации в стандарте, в которой говорится, что реализации должны гарантировать, что даже релаксированные атомарные хранилища будут немедленно видны всем потокам. 32.4.12 Реализации должны делать атомарные хранилища видимыми для атомарных загрузок в разумные сроки. open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf Но вы правы, это не так на самом деле гарантировать это, я забыл об этом. Поэтому я думаю, что этот ответ вдвойне неверен, потому что atomic<> в стандарте ISO не полностью гарантирует очистку кеша в непоследовательной системе. - person Peter Cordes; 05.07.2019

Рассмотрим операцию сравнения и обмена:

bool a = ...;
bool b = ...;

if (a)
    swap(a,b);

После того, как мы прочитаем a, мы получим true, другой поток может прийти и установить false, затем мы меняем местами (a,b), так что после выхода b становится false, даже несмотря на то, что обмен был сделан.

Используя std::atomic::compare_exchange, мы можем выполнить всю логику if/swap атомарно, чтобы другой поток не мог установить значение false между if и swap (без блокировки). В таком случае, если обмен был сделан, то b должно быть ложным на выходе.

Это всего лишь один пример атомарной операции, которая применяется к типу с двумя значениями, такому как bool.

person Andrew Tomazos    schedule 01.05.2013
comment
Почему это ответ с самым низким рейтингом? Это (или test_and_set в std::atomic_flag) является основной причиной использования типа atomic bool. - person Szocske; 02.12.2015

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

Херб Саттер сделал отличный доклад об этом, который вы можете посмотреть в Интернете. Предупреждаю, это долгий и запутанный разговор. Херб Саттер, Атомное оружие. Проблема сводится к тому, чтобы избежать гонок данных, потому что это позволяет создать иллюзию последовательной согласованности.

person huskerchad    schedule 01.05.2013

Атомарность некоторых типов зависит исключительно от базового оборудования. Каждая архитектура процессора имеет разные гарантии атомарности определенных операций. Например:

Процессор Intel486 (и более новые процессоры с тех пор) гарантирует, что следующие основные операции с памятью всегда будут выполняться атомарно:

  • Чтение или запись байта
  • Чтение или запись слова, выровненного по 16-битной границе
  • Чтение или запись двойного слова, выровненного по 32-битной границе

Другие архитектуры имеют другие спецификации, в которых операции являются атомарными.

C++ — это язык программирования высокого уровня, который стремится абстрагироваться от базового оборудования. По этой причине стандарт просто не может позволить полагаться на такие низкоуровневые предположения, иначе ваше приложение не будет переносимым. Соответственно, все типы-примитивы в C++ поставляются с atomic аналогами стандартной библиотеки, совместимой с C++11, из коробки.

person Alexander Shukaev    schedule 01.05.2013
comment
Другая важная часть заключается в том, что компиляторам C++ обычно разрешается хранить переменные в регистрах или оптимизировать доступ к ним, поскольку они могут предположить, что никакие другие потоки не изменяют значение. (Из-за гонки данных UB). atomic как бы включает это свойство volatile, поэтому while(!var){} не может оптимизироваться в if(!var) infinite_loop();. См. раздел Программирование MCU — оптимизация C++ O2 прерывается при выполнении цикла - person Peter Cordes; 03.07.2019