Свободные функции совместимы с языком С, то есть во всех случаях принимают указатели, а не ссылки. Например, первый параметр функций-членов 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 p;
void process_global_data() {
std::shared_ptr local = std::atomic_load(&p);
process_data(local);
}
void update_global_data() {
std::shared_ptr local(new my_data);
std::atomic_store(&p, local);
}
Как и для атомарных операций над другими типами, предоставляются _explicit
-варианты, позволяющие задать необходимое упорядочение, а для проверки того, используется ли в реализации внутренняя блокировка, имеется функция std::atomic_is_lock_free()
.
Как отмечалось во введении, стандартные атомарные типы позволяют не только избежать неопределённого поведения, связанного с гонкой за данные; они еще дают возможность задать порядок операций в потоках. Принудительное упорядочение лежит в основе таких средств защиты данных и синхронизации операций, как std::mutex
и std::future<>
. Помня об этом, перейдём к материалу, составляющему главное содержание этой главы: аспектам модели памяти, относящимся к параллелизму, и тому, как с помощью атомарных операций можно синхронизировать данные и навязать порядок доступа к памяти.
5.3. Синхронизация операций и принудительное упорядочение
Пусть имеются два потока, один из которых заполняет структуру данных, а другой читает ее. Чтобы избежать проблематичного состояния гонки, первый поток устанавливает флаг, означающий, что данные готовы, а второй не приступает к чтению данных, пока этот флаг не установлен. Описанный сценарий демонстрируется в листинге ниже.
Листинг 5.2.Запись и чтение переменной в разных потоках
#include
#include
#include
std::vector data;
std::atomic data_ready(false);
void reader_thread() {
while (!data_ready.load()) { ←
(1)
std::this_thread::sleep(std::milliseconds(1));
}
std::cout << "Ответ=" << data[0] << "\n";←
(2)
}
void writer_thread() {
data.push_back(42); ←
(3)
data_ready = true; ←
(4)
}
Оставим пока в стороне вопрос о неэффективности цикла ожидания готовности данных (1). Для работы этой программы он действительно необходим, потому что в противном случае разделение данных между потоками становится практически бесполезным: каждый элемент данных должен быть атомарным. Вы уже знаете, что неатомарные операции чтения (2)и записи (3)одних и тех же данных без принудительного упорядочения приводят к неопределённому поведению, поэтому где-то упорядочение должно производиться, иначе ничего работать не будет.
Требуемое упорядочение обеспечивают операции с переменной data_ready
типа std::atomic
и делается это благодаря отношениям происходит-раньше и синхронизируется-с , заложенным в модель памяти. Запись данных (3)происходит-раньше записи флага data_ready
(4), а чтение флага (1)происходит-раньше чтения данных (2). Когда прочитанное значение data_ready
(1)равно true
, операция записи синхронизируется-с этой операцией чтения, что приводит к порождению отношения происходит-раньше. Поскольку отношение происходит-раньше транзитивно, то запись данных (3)происходит-раньше записи флага (4), которая происходит-раньше чтения значения true
из этого флага (1), которое в свою очередь происходит-раньше чтения данных (2). И таким образом мы получаем принудительное упорядочение: запись данных происходит-раньше чтения данных, и программа работает правильно. На рис. 5.2 изображены важные отношения происходит-раньше в обоих потоках. Я включил две итерации цикла while
в потоке-читателе.
Читать дальше