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

Являются фундаментальными типами C/С++, такими как int, double и т.д., атомарными, например. потокобезопасны?

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

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

Ответ 1

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

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

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


Как указано @Josh, std::atomic_flag является атомным булевым типом. гарантированно не блокируется, в отличие от специализированных std::atomic.


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

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

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


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

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. Должны быть частичные специализации указателя шаблона атомного класса. Эти специализации должны иметь стандартную компоновку, тривиальные конструкторы по умолчанию и тривиальные деструкторы. Каждый из них должен поддерживать синтаксис синтаксического синтаксиса.

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

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

Ответ 2

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

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

...

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

и

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

...

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

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

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

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

Ответ 3

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

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

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

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

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

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

i += 1;

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

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

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

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

i = 3;

store i, #3;

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

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

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

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

ЦП может также переупорядочить ваши обращения к памяти в соответствии с ограничениями памяти для этого ЦП. Вы можете найти ограничения порядка памяти для архитектуры x86 в Руководстве разработчика программного обеспечения Intel 64 и IA32 Architecture, начиная с стр. 2212.

Примитивные типы (int, char и т.д.) не Atomic

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

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

Ответ 4

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

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

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