5.3. Синхронизация операций и принудительное упорядочение

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

Листинг 5.2. Запись и чтение переменной в разных потоках

#include <vector>

#include <atomic>

#include <iostream>

std::vector<int> data;

std::atomic<bool> data_ready(false);

void reader_thread() {

 while (!data_ready.load()) {            ← (1)

  std::this_thread::sleep(std::milliseconds(1));

 }

 std::cout << "Ответ=" << data[0] << " ";← (2)

}

void writer_thread() {

 data.push_back(42); ← (3)

 data_ready = true;  ← (4)

}

Оставим пока в стороне вопрос о неэффективности цикла ожидания готовности данных (1). Для работы этой программы он действительно необходим, потому что в противном случае разделение данных между потоками становится практически бесполезным: каждый элемент данных должен быть атомарным. Вы уже знаете, что неатомарные операции чтения (2) и записи (3) одних и тех же данных без принудительного упорядочения приводят к неопределённому поведению, поэтому где-то упорядочение должно производиться, иначе ничего работать не будет.

Требуемое упорядочение обеспечивают операции с переменной data_ready типа std::atomic<bool> и делается это благодаря отношениям происходит-раньше и синхронизируется-с, заложенным в модель памяти. Запись данных (3) происходит-раньше записи флага data_ready (4), а чтение флага (1) происходит-раньше чтения данных (2). Когда прочитанное значение data_ready (1) равно true, операция записи синхронизируется-с этой операцией чтения, что приводит к порождению отношения происходит-раньше. Поскольку отношение происходит-раньше транзитивно, то запись данных (3) происходит-раньше записи флага (4), которая происходит-раньше чтения значения true из этого флага (1), которое в свою очередь происходит-раньше чтения данных (2). И таким образом мы получаем принудительное упорядочение: запись данных происходит-раньше чтения данных, и программа работает правильно. На рис. 5.2 изображены важные отношения происходит-раньше в обоих потоках. Я включил две итерации цикла while в потоке-читателе.

Рис. 5.2. Принудительное задание упорядочения неатомарных операций с помощью атомарных

Все это может показаться интуитивно очевидным — разумеется, операция записи значения происходит раньше операции его чтения! В случае атомарных операций по умолчанию это действительно так (на то и умолчания), однако подчеркну: у атомарных операций есть и другие возможности для задания требований к упорядочению, и скоро я о них расскажу.

Теперь, когда вы видели, как отношения происходит-раньше и синхронизируется-с работают на практике, имеет смысл поговорить о том, что же за ними стоит. Начнем с отношения синхронизируется-с.