Являются ли фундаментальные типы 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.7), а другой читает или изменяет одно и то же место памяти.
- Библиотека определяет ряд атомных операций (раздел 29) и операции над мьютексами (статья 30), которые специально идентифицируются как операции синхронизации. Эти операции играют особую роль в назначении назначений в одном потоке, видимом другому. Операция синхронизации в одном или нескольких ячейках памяти представляет собой либо операцию потребления, операцию получения, операцию освобождения, либо операцию получения и освобождения. Операция синхронизации без привязанной ячейки памяти является ограждением и может быть либо заборным ограждением, заборным зазором, либо как заборным, так и освобождающим забором. Кроме того, существуют расслабленные атомные операции, которые не являются операциями синхронизации, и атомарные операции чтения-модификации-записи, которые имеют особые характеристики.
- Два действия потенциально параллельны, если
(23.1) - они выполняются разными потоками или
(23.2) - они не имеют последовательности, и по меньшей мере один выполняется обработчиком сигналов.
Выполнение программы содержит гонку данных, если она содержит два потенциально параллельных конфликтных действия, по крайней мере один из которых не является атомарным, и не происходит до другого, за исключением специального случая для обработчиков сигналов, описанных ниже. Любая такая гонка данных приводит к поведению undefined.
29.5 Атомные типы
- Должны быть явные специализации атомного шаблона для интегральных типов `` 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.
- Должны быть частичные специализации указателя шаблона атомного класса. Эти специализации должны иметь стандартную компоновку, тривиальные конструкторы по умолчанию и тривиальные деструкторы. Каждый из них должен поддерживать синтаксис синтаксического синтаксиса.
29.7 Тип и операции флага
- Операции над объектом типа 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
делает код менее эффективным, если он действительно необходим для правильности на платформе. Поэтому нет причин, чтобы избежать этого.