Отметим, что несмотря на то, что типы std::atomic и std::atomic формально разрешены, так как встроенные типы с плавающей точкой удовлетворяют сформулированным выше критериям на использование memcpy и memcmp, их поведение в части функции compare_exchange_strong может оказаться неожиданным. Операция может завершиться отказом, даже если ранее сохраненное значение численно равно ожидаемому, но имеет другое внутреннее представление. Отметим также, что над числами с плавающей точкой не определены атомарные арифметические операции. Аналогичное поведение compare_exchange_strong вы получите, если конкретизируете std::atomic<> пользовательским типом, в котором оператор сравнения на равенство определён, но отличается от сравнения с помощью memcmp — операция может завершиться отказом, потому что равные значения имеют различное представление.
Если размер пользовательского типа UDT равен (или меньше) размеру int или void*, то на большинстве платформ для типа std::atomic можно сгенерировать код, содержащий только атомарные команды. На некоторых платформах подобный код можно сгенерировать и в случае, когда размер пользовательского типа в два раза превышает размер int или void*. Обычно это платформы, на которых имеется команда сравнения и обмена двойных слов compare_exchange_xxx.
В главе 7 мы увидим, что такая поддержка может быть полезна для написания кода без блокировок. В силу описанных ограничений вы не можете создать, к примеру, тип std::atomic, но можете использовать для параметризации классы, содержащие счетчики, флаги, указатели и даже массивы простых элементов. Обычно это не проблема; чем сложнее структура данных, тем больше вероятность, что в ней нужно будет определить какие-то другие операции, помимо простейшего присваивания и сравнения. Но в таком случае лучше воспользоваться классом std::mutex, который гарантирует надлежащую защиту данных при выполнении этих операций (см. главу 3).
Интерфейс шаблона std::atomic, конкретизированного пользовательским типом T, ограничен набором операций, доступных классу std::atomic: load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong(), присваивание значения типа T и преобразование в значение типа T.
В табл. 5.3 перечислены операции, доступные для всех атомарных типов.
Таблица 5.3. Операции над атомарными типами
| Операция | atomic_ flag | atomic | atomic | atomic | atomic |
|---|---|---|---|---|---|
test_and_set | √ | ||||
clear | √ | ||||
is_lock_free | √ | √ | √ | √ | |
load | √ | √ | √ | √ | |
store | √ | √ | √ | √ | |
exchange | √ | √ | √ | √ | |
compare_exchange_weak, compare_exchange_strong | √ | √ | √ | √ | |
fetch_add, += | √ | √ | |||
fetch_sub, -= | √ | √ | |||
fetch_or, |= | √ | ||||
fetch_and, &= | √ | ||||
fetch_xor, ^= | √ | ||||
++, -- | √ | √ |
5.2.7. Свободные функции для атомарных операций
До сих пор я описывал только те операции над атомарными типами, которые реализованы функциями-членами. Однако для всех этих операций существуют также эквивалентные функции, не являющиеся членами классов. Как правило, имена свободных функций строятся по единому образцу: имя соответствующей функции-члена с префиксом atomic_ (например, std::atomic_load()). Затем эти функции перегружаются для каждого атомарного типа. Если имеется возможность задать признак упорядочения доступа к памяти, то предлагаются две разновидности функции: одна без признака, другая — ее имя заканчивается суффиксом _explicit — с одним или несколькими дополнительными параметрами для задания признаков (например, std::atomic_store(&atomic_var, new_value) и std::atomic_store_explicit(&atomic_var, new_value, std::memory_order_release). Если в случае функций-членов объект атомарного типа задается неявно, то все свободные функции принимают в первом параметре указатель на такой объект.
Например, для функции std::atomic_is_lock_free() есть только одна разновидность (хотя и перегруженная для всех типов), причём std::atomic_is_lock_free(&a) возвращает то же значение, что a.is_lock_free() для объекта а атомарного типа. Аналогично std::atomic_load(&a) — то же самое, что a.load(), а эквивалентом a.load(std::memory_order_acquire) является std::atomic_load_explicit(&a, std::memory_order_acquire).