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).

Свободные функции совместимы с языком С, то есть во всех случаях принимают указатели, а не ссылки. Например, первый параметр функций-членов compare_exchange_weak() и compare_exchange_strong() (ожидаемое значение) — ссылка, но вторым параметром std::atomic_compare_exchange_weak() (первый — это указатель на объект) является указатель. Функция std::atomic_compare_exchange_weak_explicit() также требует задания двух параметров, определяющих упорядочение доступа к памяти в случае успеха и отказа, тогда как функции-члены для сравнения с обменом имеют варианты как с одним параметром (второй по умолчанию равен std::memory_order_seq_cst), так и с двумя.

Операции над типом std::atomic_flag нарушают традицию, поскольку в именах функций присутствует дополнительное слово «flag»: std::atomic_flag_test_and_set(), std::atomic_flag_clear(), но у вариантов с параметрами, задающими упорядочение доступа, суффикс _explicit по-прежнему имеется: std::atomic_flag_test_and_set_explicit() и std::atomic_flag_clear_explicit().

В стандартной библиотеке С++ имеются также свободные функции для атомарного доступа к экземплярам типа std::shared_ptr<>. Это отход от принципа, согласно которому атомарные операции поддерживаются только для атомарных типов, поскольку тип std::shared_ptr<> заведомо не атомарный. Однако комитет по стандартизации С++ счел этот случай достаточно важным, чтобы предоставить дополнительные функции. К числу определенных для него атомарных операций относятся загрузка, сохранение, обмен и сравнение с обменом, и реализованы они в виде перегрузок тех же операций над стандартными атомарными типами, в которых первым аргументом является указатель std::shared_ptr<>*:

std::shared_ptr<my_data> p;

void process_global_data() {

 std::shared_ptr<my_data> local = std::atomic_load(&p);

 process_data(local);

}

void update_global_data() {

 std::shared_ptr<my_data> local(new my_data);

 std::atomic_store(&p, local);

}

Как и для атомарных операций над другими типами, предоставляются _explicit-варианты, позволяющие задать необходимое упорядочение, а для проверки того, используется ли в реализации внутренняя блокировка, имеется функция std::atomic_is_lock_free().

Как отмечалось во введении, стандартные атомарные типы позволяют не только избежать неопределённого поведения, связанного с гонкой за данные; они еще дают возможность задать порядок операций в потоках. Принудительное упорядочение лежит в основе таких средств защиты данных и синхронизации операций, как std::mutex и std::future<>. Помня об этом, перейдём к материалу, составляющему главное содержание этой главы: аспектам модели памяти, относящимся к параллелизму, и тому, как с помощью атомарных операций можно синхронизировать данные и навязать порядок доступа к памяти.