Свободные функции совместимы с языком С, то есть во всех случаях принимают указатели, а не ссылки. Например, первый параметр функций-членов 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
void process_global_data() {
std::shared_ptr
process_data(local);
}
void update_global_data() {
std::shared_ptr
std::atomic_store(&p, local);
}
Как и для атомарных операций над другими типами, предоставляются _explicit-варианты, позволяющие задать необходимое упорядочение, а для проверки того, используется ли в реализации внутренняя блокировка, имеется функция std::atomic_is_lock_free().
Как отмечалось во введении, стандартные атомарные типы позволяют не только избежать неопределённого поведения, связанного с гонкой за данные; они еще дают возможность задать порядок операций в потоках. Принудительное упорядочение лежит в основе таких средств защиты данных и синхронизации операций, как std::mutex и std::future<>. Помня об этом, перейдём к материалу, составляющему главное содержание этой главы: аспектам модели памяти, относящимся к параллелизму, и тому, как с помощью атомарных операций можно синхронизировать данные и навязать порядок доступа к памяти.
5.3. Синхронизация операций и принудительное упорядочение
Пусть имеются два потока, один из которых заполняет структуру данных, а другой читает ее. Чтобы избежать проблематичного состояния гонки, первый поток устанавливает флаг, означающий, что данные готовы, а второй не приступает к чтению данных, пока этот флаг не установлен. Описанный сценарий демонстрируется в листинге ниже.
Листинг 5.2. Запись и чтение переменной в разных потоках
#include
#include
#include
std::vector
std::atomic
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) одних и тех же данных без принудительного упорядочения приводят к неопределённому поведению, поэтому где-то упорядочение должно производиться, иначе ничего работать не будет.