Atomic c что это
Перейти к содержимому

Atomic c что это

  • автор:

std::atomic. Модель памяти C++ в примерах

Для написания эффективных и корректных многопоточных приложений очень важно знать какие существуют механизмы синхронизации памяти между потоками исполнения, какие гарантии предоставляют элементы многопоточного программирования, такие как мьютекс, join потока и другие. Особенно это касается модели памяти C++, которая была создана сложной таковой, чтобы обеспечивать оптимальный многопоточный код под множество архитектур процессоров. Кстати, язык программирования Rust, будучи построенным на LLVM, использует модель памяти такую же, как в C++. Поэтому материал в этой статье будет полезен программистам на обоих языках. Но все примеры будут на языке C++. Я буду рассказывать про std::atomic , std::memory_order и на каких трех слонах стоят атомики.

В стандарте C++11 появилась возможность писать многопоточные программы на C++, используя только стандартные средства языка. В то время многоядерные процессоры уже завоевали рынок. Особенность выполнения программы на многоядерном процессоре в том, что инструкции программы из разных потоков физически могут исполняться одновременно. Ранее многопоточность на одном ядре эмулировалась частым переключением контекста исполнения с одного потока на последующие. Для оптимизации работы с памятью у каждого ядра имеется его личный кэш памяти, над ним стоит общий кэш памяти процессора, далее оперативная память. Задача синхронизации памяти между ядрами — поддержка консистентного представления данных на каждом ядре (читай в каждом потоке). Очевидно, что если применить строгую упорядоченность изменений памяти, то операции на разных ядрах уже не будут выполнятся параллельно: остальные ядра будут ожидать, когда одно ядро выполнит инструкции изменения данных. Поэтому процессоры поддерживают работу с памятью с менее строгими гарантиями консистентности памяти. Более того, разработчику программы предоставляется выбор, какие гарантии по доступу к памяти из разных потоков требуются для достижения максимальной корректности и производительности многопоточной программы. Задача предоставить разные гарантии по памяти решалась по-разному для разных архитектур процессоров. Наиболее популярные архитектуры x86-64 и ARM имеют разные представления о том, как синхронизировать память.

Язык C++ компилируется под множество архитектур, поэтому в вопросе синхронизации данных между потоками в С++11 была добавлена модель памяти, которая обобщает механизмы синхронизации различных архитектур, позволяя генерировать для каждого процессора оптимальный код с необходимой степенью синхронизации.

Отсюда следует несколько важных выводов: модель синхронизации памяти C++ — это «искусственные» правила, которые учитывают особенности различных архитектур процессоров. В модели C++ некоторые конструкции, описанные стандартом как undefined behavior (UB), могут корректно работать на одной архитектуре, но приводить к ошибкам работы с памятью на других архитектурах.

Наша задача, как разработчиков на языке C++, состоит в том, чтобы писать корректный с точки зрения стандарта языка код. В этом случае мы можем быть уверены, что для каждой платформы будет сгенерирован корректный машинный код.

Код каждого потока компилируется и выполняется так, как будто он один в программе. Вся синхронизация данных между потоками возложена на плечи атомиков ( std::atomic ), т.к. именно они предоставляют возможность форсировать «передачу» изменений данных в другой поток. Далее я покажу, что мьютексы ( std::mutex ) и другие многопоточные примитивы либо реализованы на атомиках, либо предоставляют гарантии, семантически похожие на атомарные операции. Поэтому ключом к написанию корректных многопоточных программ является понимание того, как конкретно работают атомики.

Три слона

На мой взгляд, основная проблема с атомиками в C++ состоит в том, что они несут сразу три функции. Так на каких же трех слонах держатся атомики?

Атомики позволяют реализовать… атомарные операции.

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

Синхронизируют память в двух и более потоках выполнения.

Атомарная операция — это операция, которую невозможно наблюдать в промежуточном состоянии, она либо выполнена либо нет. Атомарные операции могут состоять из нескольких операций. Если говорить про тип std::atomic, то он предоставляет ряд примитивных операций: load , store , fetch_add , compare_exchange_* и другие. Последние две операции — это read-modify-write операции, атомарность которых обеспечивается специальными инструкциями процессора.

Рассмотрим простой пример read-modify-write операции, а именно прибавление к числу единицы. Пример 0, link:

В случае с обычной переменной v1 типа int имеем три отдельных операций: read-modify-write. Нет гарантий, что другое ядро процессора не выполняет другой операции над v1 . Операция над v2 в машинных кодах представлена как одна операция с lock сигналом на уровне процессора, гарантирующим, что к кэш линии, в которой лежит v2 , эксклюзивно имеет доступ только ядро, выполняющее эту инструкцию.

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

Про синхронизацию данных между потоками. Если мы хотим изменить данные в одном потоке и сделать так, чтобы эти изменения были видны в другом потоке, то нам необходимы примитивы многопоточного программирования. Фундаментальным таким примитивом являются атомики, остальные, например мьютексы, либо реализованы на основе атомиков, либо повторяют семантику атомиков. Все попытки записывать и читать одни и те же данные из разных потоков без примитивов синхронизации могут приводить к UB.

Случаи, когда синхронизация памяти не требуется:

Если все потоки, работающие с одним участком памяти, используют ее только на чтение

Если разные потоки используют эксклюзивно разные участки памяти

Далее будет рассмотрены более сложные случаи, когда требуется чтение и запись одного участка памяти из разных потоков. Язык C++ предоставляет три способа синхронизации памяти. По мере возрастания строгости: relaxed , release/acquire и sequential consistency . Рассмотрим их.

Неделимый, но расслабленный

Самый простой для понимания флаг синхронизации памяти — relaxed . Он гарантирует только свойство атомарности операций, при этом не может участвовать в процессе синхронизации данных между потоками. Свойства:

модификация переменной «появится» в другом потоке не сразу

поток thread2 «увидит» значения одной и той же переменной в том же порядке, в котором происходили её модификации в потоке thread1

порядок модификаций разных переменных в потоке thread1 не сохранится в потоке thread2

Можно использовать relaxed модификатор в качестве счетчика. Пример 1, link:

Использование в качестве флага остановки. Пример 2, link:

В данном примере не важен порядок в котором thread1 увидит изменения из потока, вызывающего stop_thread1 . Также не важно то, чтобы thread1 мгновенно (синхронно) увидел выставление флага stopped в true .

Пример неверного использования relaxed в качестве флага готовности данных. Пример 3, link:

Тут нет гарантий, что поток thread2 увидит изменения data ранее, чем изменение флага ready , т.к. синхронизацию памяти флаг relaxed не обеспечивает.

Полный порядок

Флаг синхронизации памяти «единая последовательность» (sequential consistency, seq_cst ) дает самые строгие. Его свойства:

порядок модификаций разных атомарных переменных в потоке thread1 сохранится в потоке thread2

все потоки будут видеть один и тот же порядок модификации всех атомарных переменных. Сами модификации могут происходить в разных потоках

все модификации памяти (не только модификации над атомиками) в потоке thread1 , выполняющей store на атомарной переменной, будут видны после выполнения load этой же переменной в потоке thread2

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

Этот флаг синхронизации памяти в C++ используется по умолчанию, т.к. с ним меньше всего проблем с точки зрения корректности выполнения программы. Но seq_cst является дорогой операцией для процессоров, в которых вычислительные ядра слабо связаны между собой в плане механизмов обеспечения консистентности памяти. Например, для x86-64 seq_cst дешевле, чем для ARM архитектур.

Продемонстрируем второе свойство. Пример 4, из книги [1], link:

После того, как все четыре потока отработают, значение переменной z будет равно 1 или 2 , потому что потоки thread_read_x_then_y и thread_read_y_then_x «увидят» изменения x и y в одном и том же порядке. От запуска к запуску это могут быть: сначала x = true , потом y = true , или сначала y = true , потом x = true .

Модификатор seq_cst всегда может быть использован вместо relaxed и acquire/release , еще и поэтому он является модификатором по умолчанию. Удобно использовать seq_cst для отладки проблем, связанных с гонкой данных в многопоточной программе: добиваемся корректной работы программы и далее заменяем seq_cst на менее строгие флаги синхронизации памяти. Примеры 1 и 2 также будут корректно работать, если заменить relaxed на seq_cst , а пример 3 начнет работать корректно после такой замены.

Синхронизация пары. Acquire/Release

Флаг синхронизации памяти acquire/release является более тонким способом синхронизировать данные между парой потоков. Два ключевых слова: memory_order_acquire и memory_order_release работают только в паре над одним атомарным объектом. Рассмотрим их свойства:

модификация атомарной переменной с release будет видна видна в другом потоке, выполняющем чтение этой же атомарной переменной с acquire

все модификации памяти в потоке thread1 , выполняющей запись атомарной переменной с release , будут видны после выполнения чтения той же переменной с acquire в потоке thread2

процессор и компилятор не могут перенести операции записи в память раньше release операции в потоке thread1 , и нельзя перемещать выше операции чтения из памяти позже acquire операции в потоке thread2

Важно понимать, что нет полного порядка между операциями над разными атомиками, происходящих в разных потоках. Например, в примере 4 если все операции store заменить на memory_order_release , а операции load заменить на memory_order_acquire , то значение z после выполнения программы может быть равно 0, 1 или 2. Это связано с тем, что, независимо от того в каком порядке по времени выполнения выполнены store для x и y , потоки thread_read_x_then_y и thread_read_y_then_x могут увидеть эти изменения в разных порядках. Кстати, такими же изменениями для load и store можно исправить пример 3. Такое изменение будет корректным и производительными, т.к. тут нам не требуется единый порядок изменений между всеми потоками (как в случае с seq_cst ), а требуется синхронизировать память между двумя потоками.

Используя release , мы даем инструкцию, что данные в этом потоке готовы для чтения из другого потока. Используя acquire , мы даем инструкцию «подгрузить» все данные, которые подготовил для нас первый поток. Но если мы делаем release и acquire на разных атомарных переменных, то получим UB вместо синхронизации памяти.

Рассмотрим реализацию простейшего мьютекса, который ожидает в цикле сброса флага для того, чтобы получить lock . Такой мьютекс называют spinlock . Это не самый эффективный способ реализации мьютекса, но он обладает всеми нужными свойствами, на которые я хочу обратить внимание. Пример 5, link:

Функция lock() непрерывно пробует сменить значение с false на true с модификатором синхронизации памяти acquire . Разница между compare_exchage_weak и strong незначительна, про нее можно почитать на cppreference. Функция unlock() выставляет значение в false с синхронизацией release . Обратите внимание, что мьютекс не только обеспечивает эксклюзивным доступ к блоку кода, который он защищает. Он также делает доступным те изменения памяти, которые были сделаны до вызова unlock() в коде, который будет работать после вызова lock() . Это важное свойство. Иногда может сложиться ошибочное мнение, что мьютекс в конкретном месте не нужен.

Рассмотрим такой пример, называемый Double Checked Locking Anti-Pattern из [2]. Пример 6, link:

Идея проста: хотим единожды в рантайме инициализировать объект Singleton . Это нужно сделать потокобезопасно, поэтому имеем мьютекс и флаг инициализации. Т.к. создается объект единожды, а используется singleton указатель в read-only режиме всю оставшуюся жизнь программы, то кажется разумным добавить предварительную проверку if (initialized) return . Данный код будет корректно работать на архитектурах процессора с более строгими гарантиями консистентности памяти, например в x86-64. Но данный код неверный с точки зрения стандарта C++. Давайте рассмотрим такой сценарий использования:

Рассмотрим следующую последовательность действий во времени:

1. сначала отрабатывает thread1 -> выполняет инициализацию под мьютексом:

lock мьютекса ( acquire )

unlock мьютекса ( release )

2. далее в игру вступает thread2 :

if(initalized) возвращает true (память, где содержится initialized могла быть неявно синхронизирована между ядрами процессора)

singleton->do_job() приводит к segmentation fault (указатель singleton не обязан был быть синхронизирован с потоком thread1 )

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

Семантика acquire/release классов стандартной библиотеки

Механизм acquire/release поможет понять гарантии синхронизации памяти, которые предоставляют классы стандартной библиотеки для работы с потоками. Ниже приведу список наиболее часто используемых операций.

std::thread::(constructor) vs функция потока

Вызов конструктора объекта std::thread ( release ) синхронизирован со стартом работы функции нового потока ( acquire ). Таким образом функция потока будет видеть все изменения памяти, которые произошли до вызова конструктора в исходном потоке.

std::thread::join vs владеющий поток

После успешного вызова join поток, в котором был вызван join, «увидит» все изменения памяти, которые были выполнены завершившимся потоком.

std::mutex::lock vs std::mutex::unlock

успешный lock синхронизирует память, которая была изменена до вызова предыдущего unlock.

std::promise::set_value vs std::future::wait

set_value синхронизирует память с успешным wait .

И так далее. Полный список можно найти в книге [1].

Что это все значит? Повторю эту важную мысль еще раз: это значит, на примере std::promise::set_value и std::future::wait , что тут мы не только получили данные, которые содержатся в примитиве синхронизации, но и нам доступны все изменения памяти, которые были в потоке до того, как он выполнил set_value . Это маленькое чудо нам кажется само собой разумеющееся с нашим бытовым, последовательным причинно-следственным, взглядом на мир. Но в мире многоядерного процессора, законы которого больше похожи на квантовую физику, которую никто до конца не понимает, нет единого последовательно порядка изменения памяти в разных ядрах процессора, если это не затребовано разработчиком явно, или неявно через многопоточные примитивы.

Заключение

Сложно представить современную C++ программу, которая была бы однопоточной. Опасно писать многопоточные программы, не имея представления о правилах синхронизации памяти. Я считаю, что нужно знать, как работают атомики в C++. Чтобы не совершать ошибок типа volatile bool , чтобы понимать, какие изменения в каких потоках будут видны после использования того или иного многопоточного примитива, чтобы использовать read-modify-write атомарные операции вместо мьютекса, там где это возможно. Данная статья помогла мне систематизировать материал, который я находил в разных источниках и освежить знания в памяти. Надеюсь, она поможет и вам!

Name already in use

cpp-docs / docs / standard-library / atomic.md

  • Go to file T
  • Go to line L
  • Copy path
  • Copy permalink
  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

Defines classes and class templates to use to create types that support atomic operations.

[!NOTE] In code that’s compiled by using /clr:pure , this header is blocked. Both /clr:pure and /clr:safe are deprecated in Visual Studio 2017 and later versions.

An atomic operation has two key properties that help you use multiple threads to correctly manipulate an object without using mutex locks.

Because an atomic operation is indivisible, a second atomic operation on the same object from a different thread can obtain the object’s state only before or after the first atomic operation.

Based on its memory_order argument, an atomic operation establishes ordering requirements for the visibility of the effects of other atomic operations in the same thread. Consequently, it inhibits compiler optimizations that violate the ordering requirements.

On some platforms, it might not be possible to efficiently implement atomic operations for some types without using mutex locks. An atomic type is lock-free if no atomic operations on that type use locks.

C++11 : In signal-handlers, you can perform atomic operations on an object obj if obj.is_lock_free() or atomic_is_lock_free(x) are true .

The class atomic_flag provides a minimal atomic type that holds a bool flag. Its operations are always lock-free.

The class template atomic<T> stores an object of its argument type T and provides atomic access to that stored value. You can instantiate it by using any type that can be copied by using memcpy and tested for equality by using memcmp . In particular, you can use it with user-defined types that meet these requirements and, in many cases, with floating-point types.

The template also has a set of specializations for integral types and a partial specialization for pointers. These specializations provide additional operations that aren’t available through the primary template.

The atomic<T *> partial specializations apply to all pointer types. They provide methods for pointer arithmetic.

The atomic<integral> specializations apply to all integral types. They provide additional operations that aren’t available through the primary template.

Each atomic<integral> type has a corresponding macro that you can use in an if directive to determine at compile time whether operations on that type are lock-free. If the value of the macro is zero, operations on the type aren’t lock-free. If the value is 1, operations might be lock-free, and a runtime check is required. If the value is 2, operations are lock-free. You can use the function atomic_is_lock_free to determine at runtime whether operations on the type are lock-free.

For each of the integral types, there’s a corresponding named atomic type that manages an object of that integral type. Each atomic_integral type has the same set of member functions as the corresponding instantiation of atomic<T> and can be passed to any of the non-member atomic functions.

atomic_integral Type Integral Type atomic_is_lock_free Macro
atomic_char char ATOMIC_CHAR_LOCK_FREE
atomic_schar signed char ATOMIC_CHAR_LOCK_FREE
atomic_uchar unsigned char ATOMIC_CHAR_LOCK_FREE
atomic_char16_t char16_t ATOMIC_CHAR16_T_LOCK_FREE
atomic_char32_t char32_t ATOMIC_CHAR32_T_LOCK_FREE
atomic_wchar_t wchar_t ATOMIC_WCHAR_T_LOCK_FREE
atomic_short short ATOMIC_SHORT_LOCK_FREE
atomic_ushort unsigned short ATOMIC_SHORT_LOCK_FREE
atomic_int int ATOMIC_INT_LOCK_FREE
atomic_uint unsigned int ATOMIC_INT_LOCK_FREE
atomic_long long ATOMIC_LONG_LOCK_FREE
atomic_ulong unsigned long ATOMIC_LONG_LOCK_FREE
atomic_llong long long ATOMIC_LLONG_LOCK_FREE
atomic_ullong unsigned long long ATOMIC_LLONG_LOCK_FREE

Typedef names exist for specializations of the atomic template for some of the types that are defined in the header <inttypes.h> .

Atomic Type Typedef Name
atomic_int8_t atomic<int8_t>
atomic_uint8_t atomic<uint8_t>
atomic_int16_t atomic<int16_t>
atomic_uint16_t atomic<uint16_t>
atomic_int32_t atomic<int32_t>
atomic_uint32_t atomic<uint32_t>
atomic_int64_t atomic<int64_t>
atomic_uint64_t atomic<uint64_t>
atomic_int_least8_t atomic<int_least8_t>
atomic_uint_least8_t atomic<uint_least8_t>
atomic_int_least16_t atomic<int_least16_t>
atomic_uint_least16_t atomic<uint_least16_t>
atomic_int_least32_t atomic<int_least32_t>
atomic_uint_least32_t atomic<uint_least32_t>
atomic_int_least64_t atomic<int_least64_t>
atomic_uint_least64_t atomic<uint_least64_t>
atomic_int_fast8_t atomic<int_fast8_t>
atomic_uint_fast8_t atomic<uint_fast8_t>
atomic_int_fast16_t atomic<int_fast16_t>
atomic_uint_fast16_ atomic<uint_fast16_t>
atomic_int_fast32_t atomic<int_fast32_t>
atomic_uint_fast32_t atomic<uint_fast32_t>
atomic_int_fast64_t atomic<int_fast64_t>
atomic_uint_fast64_t atomic<uint_fast64_t>
atomic_intptr_t atomic<intptr_t>
atomic_uintptr_t atomic<uintptr_t>
atomic_size_t atomic<size_t>
atomic_ptrdiff_t atomic<ptrdiff_t>
atomic_intmax_t atomic<intmax_t>
atomic_uintmax_t atomic<uintmax_t>
Name Description
atomic Structure Describes an object that performs atomic operations on a stored value.
atomic_flag Structure Describes an object that atomically sets and clears a bool flag.
Name Description
memory_order Enum Supplies symbolic names for synchronization operations on memory locations. These operations affect how assignments in one thread become visible in another.

In the following list, the functions that don’t end in _explicit have the semantics of the corresponding _explicit , except that they have the implicit memory_order arguments of memory_order_seq_cst .

What exactly is std::atomic?

I understand that std::atomic<> is an atomic object. But atomic to what extent? To my understanding an operation can be atomic. What exactly is meant by making an object atomic? For example if there are two threads concurrently executing the following code:

Then is the entire operation (say add_twelve_to(int) ) atomic? Or are changes made to the variable atomic (so operator=() )?

3 Answers 3

Each instantiation and full specialization of std::atomic<> represents a type that different threads can simultaneously operate on (their instances), without raising undefined behavior:

Objects of atomic types are the only C++ objects that are free from data races; that is, if one thread writes to an atomic object while another thread reads from it, the behavior is well-defined.

In addition, accesses to atomic objects may establish inter-thread synchronization and order non-atomic memory accesses as specified by std::memory_order .

std::atomic<> wraps operations that, in pre-C++ 11 times, had to be performed using (for example) interlocked functions with MSVC or atomic bultins in case of GCC.

Also, std::atomic<> gives you more control by allowing various memory orders that specify synchronization and ordering constraints. If you want to read more about C++ 11 atomics and memory model, these links may be useful:

Note that, for typical use cases, you would probably use overloaded arithmetic operators or another set of them:

Because operator syntax does not allow you to specify the memory order, these operations will be performed with std::memory_order_seq_cst , as this is the default order for all atomic operations in C++ 11. It guarantees sequential consistency (total global ordering) between all atomic operations.

In some cases, however, this may not be required (and nothing comes for free), so you may want to use more explicit form:

Now, your example:

will not evaluate to a single atomic op: it will result in a.load() (which is atomic itself), then addition between this value and 12 and a.store() (also atomic) of final result. As I noted earlier, std::memory_order_seq_cst will be used here.

However, if you write a += 12 , it will be an atomic operation (as I noted before) and is roughly equivalent to a.fetch_add(12, std::memory_order_seq_cst) .

As for your comment:

A regular int has atomic loads and stores. Whats the point of wrapping it with atomic<> ?

Your statement is only true for architectures that provide such guarantee of atomicity for stores and/or loads. There are architectures that do not do this. Also, it is usually required that operations must be performed on word-/dword-aligned address to be atomic std::atomic<> is something that is guaranteed to be atomic on every platform, without additional requirements. Moreover, it allows you to write code like this:

Note that assertion condition will always be true (and thus, will never trigger), so you can always be sure that data is ready after while loop exits. That is because:

  • store() to the flag is performed after sharedData is set (we assume that generateData() always returns something useful, in particular, never returns NULL ) and uses std::memory_order_release order:

memory_order_release

A store operation with this memory order performs the release operation: no reads or writes in the current thread can be reordered after this store. All writes in the current thread are visible in other threads that acquire the same atomic variable

  • sharedData is used after while loop exits, and thus after load() from flag will return a non-zero value. load() uses std::memory_order_acquire order:

std::memory_order_acquire

A load operation with this memory order performs the acquire operation on the affected memory location: no reads or writes in the current thread can be reordered before this load. All writes in other threads that release the same atomic variable are visible in the current thread.

This gives you precise control over the synchronization and allows you to explicitly specify how your code may/may not/will/will not behave. This would not be possible if only guarantee was the atomicity itself. Especially when it comes to very interesting sync models like the release-consume ordering.

std::atomic

Each instantiation and full specialization of the std::atomic template defines an atomic type. If one thread writes to an atomic object while another thread reads from it, the behavior is well-defined (see memory model for details on data races).

In addition, accesses to atomic objects may establish inter-thread synchronization and order non-atomic memory accesses as specified by std::memory_order .

std::atomic is neither copyable nor movable.

The compatibility macro _Atomic is provided in <stdatomic.h> such that _Atomic(T) is identical to std::atomic<T> while both are well-formed.

It is unspecified whether any declaration in namespace std is available when <stdatomic.h> is included.

Specializations

Primary template

The primary std::atomic template may be instantiated with any TriviallyCopyable type T satisfying both CopyConstructible and CopyAssignable. The program is ill-formed if any of following values is false :

  • std:: is_trivially_copyable < T > :: value
  • std:: is_copy_constructible < T > :: value
  • std:: is_move_constructible < T > :: value
  • std:: is_copy_assignable < T > :: value
  • std:: is_move_assignable < T > :: value

std::atomic<bool> uses the primary template. It is guaranteed to be a standard layout struct.

Partial specializations

The standard library provides partial specializations of the std::atomic template for the following types with additional properties that the primary template does not have:

2) Partial specializations std::atomic<U*> for all pointer types. These specializations have standard layout , trivial default constructors, (until C++20) and trivial destructors. Besides the operations provided for all atomic types, these specializations additionally support atomic arithmetic operations appropriate to pointer types, such as fetch_add , fetch_sub .

3-4) Partial specializations std :: atomic < std:: shared_ptr < U >> and std :: atomic < std:: weak_ptr < U >> are provided for std::shared_ptr and std::weak_ptr .

Specializations for integral types

When instantiated with one of the following integral types, std::atomic provides additional atomic operations appropriate to integral types such as fetch_add , fetch_sub , fetch_and , fetch_or , fetch_xor :

  • The character types char , char8_t (since C++20) , char16_t , char32_t , and wchar_t ;
  • The standard signed integer types: signed char , short , int , long , and long long ;
  • The standard unsigned integer types: unsigned char , unsigned short , unsigned int , unsigned long , and unsigned long long ;
  • Any additional integral types needed by the typedefs in the header <cstdint> .

Additionally, the resulting std::atomic<Integral> specialization has standard layout , a trivial default constructor, (until C++20) and a trivial destructor. Signed integer arithmetic is defined to use two’s complement; there are no undefined results.

Specializations for floating-point types

When instantiated with one of the standard floating-point types float , double , and long double , or one extended floating-point type (since C++23) , std::atomic provides additional atomic operations appropriate to floating-point types such as fetch_add and fetch_sub .

Additionally, the resulting std::atomic<Floating> specialization has standard layout and a trivial destructor.

No operations result in undefined behavior even if the result is not representable in the floating-point type. The floating-point environment in effect may be different from the calling thread’s floating-point environment.

Type aliases

Type aliases are provided for bool and all integral types listed above, as follows:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *