5.2.1. Стандартные атомарные типы

Все стандартные атомарные типы определены в заголовке <atomic>. Любые операции над такими типами атомарны, и только операции над этими типами атомарны в смысле принятого в языке определения, хотя мьютексы позволяют реализовать кажущуюся атомарность других операций. На самом деле, и сами стандартные атомарные типы могут пользоваться такой эмуляцией: почти во всех имеется функция-член is_lock_free(), которая позволяет пользователю узнать, выполняются ли операции над данным типом с помощью действительно атомарных команд (x.is_lock_free() возвращает true) или с применением некоторой внутренней для компилятора и библиотеки блокировки (x.is_lock_free() возвращает false).

Единственный тип, в котором функция-член is_lock_free() отсутствует, — это std::atomic_flag. В действительности это по-настоящему простой булевский флаг, а операции над этим типом обязаны быть свободными от блокировок; если имеется простой свободный от блокировок булевский флаг, то на его основе можно реализовать простую блокировку и, значит, все остальные атомарные типы. Говоря по-настоящему простой, я именно это и имел в виду: после инициализации объект типа std::atomic_flag сброшен, и для него определены всего две операции: проверить и установить (функция-член test_and_set()) и очистить (функция-член clear()). Это всё — нет ни присваивания, ни копирующего конструктора, ни операции «проверить и очистить», вообще ничего больше.

Доступ ко всем остальным атомарным типам производится с помощью специализаций шаблона класса std::atomic<>; их функциональность несколько богаче, но они необязательно свободны от блокировок (как было объяснено выше). На самых распространенных платформах можно ожидать, что атомарные варианты всех встроенных типов (например, std::atomic<int> и std::atomic<void*>) действительно будут свободны от блокировок, но такого требования не предъявляется. Как мы скоро увидим, интерфейс каждой специализации отражает свойства типа; например, поразрядные операции, например &=, не определены для простых указателей, поэтому они не определены и для атомарных указателей.

Помимо прямого использования шаблона класса std::atomic<>, разрешается использовать имена, приведённые в табл. 5.1, которые ссылаются на определенные в конкретной реализации атомарные типы. Из-за исторических особенностей добавления атомарных типов в стандарт С++ альтернативные имена типов могут ссылаться либо на соответствующую специализацию std::atomic<>, либо на базовый класс этой специализации. Поэтому смешение альтернативных имен и прямых имен специализаций std::atomic<> может сделать программу непереносимой.

Таблица 5.1. Альтернативные имена стандартных атомарных типов и соответствующие им специализации std::atomic<>

Атомарный тип Соответствующая специализация atomic_bool std::atomic<bool> atomic_char std::atomic<char> atomic_schar std::atomic<signed char> atomic_uhar std::atomic<unsigned char> atomic_int std::atomic<int> atomic_uint std::atomic<unsigned> atomic_short std::atomic<short> atomic_ushort std::atomic<unsigned short> atomic_long std::atomic<long> atomic_ulong std::atomic<unsigned long> atomic_llong std::atomic<long long> atomic_ullong std::atomic<unsigned long long> atomic_char16_t std::atomic<char16_t> atomic_char32_t std::atomic<char32_t> atomic_wchar_t std::atomic<wchar_t>

Помимо основных атомарных типов, в стандартной библиотеке С++ определены также псевдонимы typedef для атомарных типов, соответствующих различным неатомарным библиотечным typedef, например std::size_t. Они перечислены в табл. 5.2.

Таблица 5.2. Соответствие между стандартными атомарными и встроенными typedef

Атомарный typedef Соответствующий typedef из стандартной библиотеки atomic_int_least8_t int_least8_t atomic_uint_least8_t uint_least8_t atomic_int_least16_t int_least16_t atomic_uint_least16_t uint_least16_t atomic_int_least32_t int_least32_t atomic_uint_least32_t uint_least32_t atomic_int_least64_t int_least64_t atomic_uint_least64_t uint_least64_t atomic_int_fast8_t int_fast8_t atomic_uint_fast8_t uint_fast8_t atomic_int_fast16_t int_fast16_t atomic_uint_fast16_t uint_fast16_t atomic_int_fast32_t int_fast32_t atomic_uint_fast32_t uint_fast32_t atomic_int_fast64_t int_fast64_t atomic_uint_fast64_t uint_fast64_t atomic_intptr_t intptr_t atomic_uintptr_t uintptr_t atomic_size_t size_t atomic_ptrdiff_t ptrdiff_t atomic_intmax_t intmax_t atomic_uintmax_t uintmax_t

Да уж, типов немало! Но есть простая закономерность — атомарный тип, соответствующий стандартному typedef T, имеет такое же имя с префиксом atomic_: atomic_T. То же самое относится и к встроенным типам с тем исключением, что signed сокращается до s, unsigned — до u, a long long — до llong. Вообще говоря, проще написать std::atomic<T> для нужного вам типа T, чем пользоваться альтернативными именами.

Стандартные атомарные типы не допускают копирования и присваивания в обычном смысле, то есть не имеют копирующих конструкторов и операторов присваивания. Однако им все же можно присваивать значения соответствующих встроенных типов, и они поддерживают неявные преобразования в соответствующие встроенные типы. Кроме того, в них определены функции-члены load(), store(), exchange(), compare_exchange_weak() и compare_exchange_strong(). Поддерживаются также составные операторы присваивания (там, где это имеет смысл) +=, -=, *=, |= и т.д., а для целочисленных типов и специализаций std::atomic<> для указателей — еще и операторы ++ и --. Этим операторам соответствуют также именованные функции-члены с идентичной функциональностью: fetch_add(), fetch_or() и т.д. Операторы присваивания возвращают сохраненное значение, а именованные функции-члены — значение, которое объект имел до начала операции. Это позволяет избежать потенциальных проблем, связанных с тем, что обычно операторы присваивания возвращают ссылку на объект в левой части. Чтобы получить из такой ссылки сохраненное значение, программа должна была бы выполнить еще одну операцию чтения, но тогда между присваиванием и чтением другой поток мог бы модифицировать значение, открывая дорогу гонке.

Но шаблон класса std::atomic<> — не просто набор специализаций. В нем есть основной шаблон, который можно использовать для создания атомарного варианта пользовательского типа. Поскольку это обобщенный шаблон класса, определены только операции load(), store() (а также присваивание значения пользовательского типа и преобразования в пользовательский тип), exchange(), compare_exchange_weak() и compare_exchange_strong().

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

• Операции сохранения, для которых можно задавать упорядочение memory_order_relaxed, memory_order_release и memory_оrder_sеq_cst.

• Операции загрузки, для которых можно задавать упорядочение memory_order_relaxed, memory_order_consume, memory_order_acquire и memory_order_seq_cst.

• Операции чтения-модификации-записи, для которых можно задавать упорядочение memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel и memory_order_seq_cst.

По умолчанию для всех операций подразумевается упорядочение memory_оrder_sеq_cst.

Теперь рассмотрим, какие операции можно производить над каждым из стандартных атомарных типов, начиная с std::atomic_flag.