Являются ли фундаментальные типы C/C++ атомарными?

являются фундаментальными типами C / C++, например int, double, etc., атомарный, например threadsafe?

свободны ли они от гонок данных; то есть, если один поток записывает в объект такого типа, а другой поток читает из него, хорошо ли определено поведение?

Если нет, зависит ли это от компилятора или чего-то еще?

4 ответов


нет, основные типы данных (например, int, double) не являются атомарными, см. std::atomic.

вместо этого вы можете использовать std::atomic<int> или std::atomic<double>.

Примечание: std::atomic был введен с C++11, и я понимаю, что до C++11 стандарт C++ вообще не признавал существования многопоточности.


как отметил @Josh,std::atomic_flag атомная булева типа. Это гарантированно будет lock-free в отличие от std::atomic специализации.


цитируемая документация из:http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4567.pdf. Я уверен, что стандарт не является бесплатным, и поэтому это не окончательная / официальная версия.

1.10 многопоточные исполнения и гонки данных

  1. конфликтуют две оценки выражений, если одна из них изменяет память местоположение (1.7), а другой считывает или изменяет то же самое местоположение памяти.
  2. библиотека определяет ряд атомарных операций (пункт 29) и операций над мьютексами (пункт 30), которые специально определены как операции синхронизации. Эти операции играют особую роль в том, чтобы сделать назначения в одном потоке видимыми для другого. Операцию синхронизации на одной или более ячеек памяти либо потреблять операции, операции acquire, операция освобождение, или как приобретите и выпустите деятельность. Операции синхронизации без связанного памяти забор и можно либо приобрести забор, Забор релиз, или как получать и освобождать забор. Кроме того, существуют расслабленные атомарные операции, которые не являются операциями синхронизации, и атомарные операции чтения-изменения-записи, которые имеют специальные характеристики.


  1. два действия потенциально параллельны если
    (23.1) - они выполняются различными потоками или
    (23.2) - они не имеют последовательности, и по крайней мере один из них выполняется обработчиком сигнала.
    Выполнение программы содержит гонку данных, если она содержит два потенциально параллельных конфликтующих действия, по крайней мере одно из которых не является атомарным, и ни одно из них не происходит раньше другого, за исключением особого случая для обработчиков сигналов, описанного ниже. Любая такая гонка данных приводит к неопределенному поведению.

29.5 атомарные типы

  1. должны быть явные специализации атомарного шаблона для интегральных типов " char,signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long, char16_t,char32_t, wchar_t и любые другие типы, необходимые typedefs в заголовке <cstdint>. Для каждого интегрального типа интеграла специализация atomic<integral> обеспечивает дополнительные атомарные операции подходит для интегральных типов. Должна быть специализация atomic<bool> который обеспечивает общие атомарные операции, указанные в 29.6.1..


  1. должны быть частичные специализации указателя шаблона атомарного класса. Эти специализации должны иметь стандартную компоновку, тривиальные конструкторы по умолчанию и тривиальные деструкторы. Каждый из них должен поддерживать синтаксис инициализации aggregate.

29.7 тип флага и операции

  1. операции с объектом типа atomic_flag не должны блокироваться. [ Примечание: следовательно, операции также должны быть свободными от адресов. Никакой другой тип не требует операций без блокировки, поэтому тип atomic_flag является минимальным аппаратно-реализованным типом, необходимым для соответствия этому международному стандарту. Остальные типы можно эмулировать с помощью atomic_flag, хотя и с менее идеальными свойствами. - конец Примечания ]

поскольку C также (в настоящее время) упоминается в вопросе, несмотря на то, что не находится в тегах,Стандарт C гласит:

выполнение программы 5.1.2.3

...

когда обработка абстрактной машины прерывается получением сигнала, значения объектов, которые не являются атомарными без блокировки объекты, ни типа volatile sig_atomic_t не указаны, как и состояние среды с плавающей запятой. Этот значение любого объекта изменено обработчиком, который не является ни атомарным объектом без блокировки, ни типа volatile sig_atomic_t становится неопределенным, когда обработчик завершает работу, как и состояние среды с плавающей запятой, если оно изменен обработчиком и не восстановлен в исходное состояние.

и

5.1.2.4 многопоточные исполнения и гонки данных

...

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

[несколько страниц стандартов - некоторые абзацы, явно касающиеся атомарных типов]

выполнение программы содержит гонки данных если он содержит два конфликтующих действия в разных потоках, по крайней мере одно из которых не является атомарным, и ни одно из них не происходит перед другим. какие такие результаты гонки данных в неопределенном поведении.

обратите внимание, что значения "неопределенны", если сигнал прерывает обработку, а одновременный доступ к типам, которые явно не являются атомарными, является неопределенным поведением.


что такое атомный?

атомарный, как описывающий что-то со свойством атома. Слово "атом" происходит от латинского атомус что означает "единовластие".

обычно я думаю об атомной операции (независимо от языка), чтобы иметь два качества:

атомная операция всегда неделима.

т. е. он выполняется неделимым образом, я считаю, что это то, что OP называет "threadsafe". В смысле операция происходит мгновенно при просмотре другим потоком.

например, следующая операция, вероятно, разделена (зависит от компилятора / оборудования):

i += 1;

потому что это может наблюдаться другим потоком (на гипотетическом оборудовании и компиляторе) как:

load r1, i;
addi r1, #1;
store i, r1;

два потока, выполняющие вышеуказанную операцию i += 1 без соответствующей синхронизации может привести к неправильному результату. Скажи i=0 изначально, нити T1 нагрузки T1.r1 = 0, и нить T2 нагрузки t2.r1 = 0. Оба потока увеличивают их соответствующие r1s на 1, а затем сохранить результат на i. Хотя было выполнено два приращения, значение i все еще только 1, потому что операция приращения была делимой. Обратите внимание, что была синхронизация до и после i+=1 другой поток подождал бы, пока операция не будет завершена, и, таким образом, наблюдал бы неделимую операцию.

обратите внимание, что даже простая запись может быть или не быть безраздельно:

i = 3;

store i, #3;

в зависимости от компилятора и аппаратуры. Например, если адрес i не выровнен соответствующим образом, тогда должна использоваться несогласованная загрузка/магазин, которая выполняется процессором как несколько меньших нагрузок/магазинов.

атомарная операция имеет гарантированную семантику упорядочения памяти.

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

для например, под "что-если" правило компилятору разрешено переупорядочивать хранилища и загрузки, как он считает нужным, пока весь доступ к изменчивой памяти происходит в порядке, указанном программой "как если бы" программа была оценена в соответствии с формулировкой в стандарте. Таким образом, неатомные операции могут быть перестроены, нарушая любые предположения о порядке выполнения в многопоточной программе. Вот почему, казалось бы, невинное использование сырья int в качестве переменной сигнализации в многопоточное программирование нарушено, даже если записи и чтения могут быть неделимыми, порядок может нарушить программу в зависимости от компилятора. Атомарная операция обеспечивает упорядочение операций вокруг нее в зависимости от того, какая семантика памяти указана. См.std::memory_order.

процессор также может переупорядочить доступ к памяти в соответствии с ограничениями упорядочения памяти этого процессора. Вы можете найти ограничения упорядочения памяти для архитектуры x86 в Intel 64 и ia32 архитектуры руководство разработчика программного обеспечения раздел 8.2, начиная со страницы 2212.

примитивные типы (int, char etc) не являются атомными

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

надеюсь, это объясняет почему примитивные типы не являются атомарными.


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

если вы используете std::atomic<bool>, например,bool фактически является атомарным в целевой архитектуре, тогда компилятор не будет генерировать избыточные ограждения или блокировки. Тот же код будет сгенерирован, что и для plain bool.

другими словами, с помощью std::atomic только делает код менее эффективным, если он действительно требуется для корректности на платформе. Так что нет причин избегать он.