Требуемое упорядочение обеспечивают операции с переменной data_ready типа std::atomic и делается это благодаря отношениям data_ready (4), а чтение флага (1) происходит-раньше чтения данных (2). Когда прочитанное значение data_ready (1) равно true, операция записи синхронизируется-с этой операцией чтения, что приводит к порождению отношения происходит-раньше. Поскольку отношение происходит-раньше транзитивно, то запись данных (3) происходит-раньше записи флага (4), которая происходит-раньше чтения значения true из этого флага (1), которое в свою очередь происходит-раньше чтения данных (2). И таким образом мы получаем принудительное упорядочение: запись данных происходит-раньше чтения данных, и программа работает правильно. На рис. 5.2 изображены важные отношения происходит-раньше в обоих потоках. Я включил две итерации цикла while в потоке-читателе.
Рис. 5.2. Принудительное задание упорядочения неатомарных операций с помощью атомарных
Все это может показаться интуитивно очевидным — разумеется, операция записи значения происходит раньше операции его чтения! В случае атомарных операций по умолчанию это действительно так (на то и умолчания), однако подчеркну: у атомарных операций есть и другие возможности для задания требований к упорядочению, и скоро я о них расскажу.
Теперь, когда вы видели, как отношения происходит-раньше и синхронизируется-с работают на практике, имеет смысл поговорить о том, что же за ними стоит. Начнем с отношения синхронизируется-с.
5.3.1. Отношение синхронизируется-с
Отношение синхронизируется-с возможно только между операциями над атомарными типами. Операции над структурой данных (например, захват мьютекса) могут обеспечить это отношение, если в структуре имеются атомарные типы и определенные в ней операции выполняют необходимые атомарные операции. Однако реальным источником синхронизации всегда являются операции над атомарными типами.
Идея такова: подходящим образом помеченная атомарная операция записи W над переменной x синхронизируется-с подходящим образом помеченной атомарной операцией чтения над переменной x, которая читает значение, сохраненное либо данной операцией записи (W), либо следующей за ней атомарной операцией записи над x в том же потоке, который выполнил первоначальную операцию W, либо последовательностью атомарных операций чтения-модификации-записи над x (например, fetch_add() или compare_exchange_weak()) в любом потоке, при условии, что значение, прочитанное первым потоком в этой последовательности, является значением, записанным операцией W (см. раздел 5.3.4).
Пока оставим в стороне слова «подходящим образом помеченная», потому что по умолчанию все операции над атомарными типами помечены подходящим образом. По существу сказанное выше означает ровно то, что вы ожидаете: если поток А сохраняет значение, а поток В читает это значение, то существует отношение синхронизируется-с между сохранением в потоке А и загрузкой в потоке В — как в листинге 5.2.
Уверен, вы догадались, что нюансы как раз и скрываются за словами «подходящим образом помеченная». Модель памяти в С++ допускает применение различных ограничений на упорядочение к операциям над атомарными типами, и именно это и называется пометкой. Варианты упорядочения доступа к памяти и их связь с отношением синхронизируется-с рассматриваются в разделе 5.3.3. А пока отступим на один шаг и поговорим об отношении происходит-раньше.
5.3.2. Отношение происходит-раньше
Отношение происходит-раньше — основной строительный блок механизма упорядочения операций в программе. Оно определяет, какие операции видят последствия других операций и каких именно. В однопоточной программе всё просто: если в последовательности выполняемых операций одна стоит раньше другой, то она и происходит-раньше. Иначе говоря, если операция А в исходном коде предшествует операции В, то А происходит-раньше В. Это мы видели в листинге 5.2: запись в переменную data (3) происходит-раньше записи в переменную data_ready (4). В общем случае между операциями, которые входят в состав одного предложения языка, нет отношения происходит-раньше, поскольку они не упорядочены. По-другому то же самое можно выразить, сказав, что порядок не определён. Мы знаем, что программа, приведённая в следующем листинге, напечатает "1,2" или "2,1", но что именно, неизвестно, потому что порядок двух обращений к get_num() не определён.
Листинг 5.3. Порядок определения аргументов функции не определён
#include
void foo(int a, int b) {
std::cout << a << "," << b << std::endl;
}
int get_num() {
static int i = 0;
return ++i;
}
int main() {