С зависимостями по данным связаны два новых отношения: int, то отношение применяется и тогда, когда результат А сохраняется в переменной, которая затем используется в качестве операнда В. Эта операция также транзитивна, то есть если А переносит-зависимость-в В и В переносит-зависимость-в С, то А переносит-зависимость-в С.
С другой стороны, отношение предшествует-по-зависимости может применяться к разным потокам. Оно вводится с помощью атомарных операций загрузки, помеченных признаком memory_order_consume. Это частный случай семантики memory_order_acquire, в котором синхронизированные данные ограничиваются прямыми зависимостями; операция сохранения А, помеченная признаком memory_order_release, memory_order_acq_rel или memory_order_seq_cst, предшествует-по-зависимости операции загрузки В, помеченной признаком memory_order_consume, если потребитель читает сохраненное значение. Это противоположность отношению синхронизируется-с, которое образуется, если операция загрузки помечена признаком memory_order_acquire. Если такая операция В затем переносит-зависимость-в некоторую операцию С, то А также предшествует-по-зависимости С.
Это не дало бы ничего полезного для целей синхронизации, если бы не было связано с отношением межпоточно происходит-раньше. Однако же справедливо следующее утверждение: если А предшествует-по-зависимости В, то А межпоточно происходит-раньше В.
Одно из важных применений такого упорядочения доступа к памяти связано с атомарной операцией загрузки указателя на данные. Пометив операцию загрузки признаком memory_order_consume, а предшествующую ей операцию сохранения — признаком memory_order_release, можно гарантировать, что данные, адресуемые указателем, правильно синхронизированы, даже не накладывая никаких требований к синхронизации с другими независимыми данными. Этот сценарий иллюстрируется в следующем листинге.
Листинг 5.10. Использование std::memory_order_consume для синхронизации данных
struct X {
int i;
std::string s;
};
std::atomic
std::atomic
void create_x() {
X* x = new X;
x->i = 42;
x->s = "hello";
a.store(99, std::memory_order_relaxed);←(1)
p.store(x, std::memory_order_release); ←(2)
}
void use_x() {
X* x;
while (!(x = p.load(std::memory_order_consume)))←(3)
std::this_thread::sleep(std::chrono::microseconds(1));
assert(x->i == 42); ←(4)
assert(x->s =="hello"); ←(5)
assert(a.load(std::memory_order_relaxed) == 99);←(6)
}
int main() {
std::thread t1(create_x);
std::thread t2(use_x);
t1.join();
t2.join();
}
Хотя сохранение а (1) расположено перед сохранением p (2) и сохранение p помечено признаком memory_order_release, но загрузка p (3) помечена признаком memory_order_consume. Это означает, что сохранение p происходит-раньше только тех выражений, которые зависят от значения, загруженного из p. Поэтому утверждения о членах-данных структуры x (4), (5) гарантированно не сработают, так как загрузка p переносит-зависимость-в эти выражения посредством переменной x. С другой стороны, утверждение о значении а (6) может как сработать, так и не сработать; эта операция не зависит от значения, загруженного из p, поэтому нет никаких гарантий о прочитанном значении. Это ясно следует из того, что она помечена признаком memory_order_relaxed.