С зависимостями по данным связаны два новых отношения: предшествует-по-зависимости (dependency-ordered-before) и переносит-зависимость-в (carries-a-dependency-to). Как и отношение расположено-перед, отношение переносит-зависимость-в применяется строго внутри одного потока и моделирует зависимость по данным между операциями — если результат операции А используется в качестве операнда операции В, то А переносит-зависимость-в В. Если результатом операции А является значение скалярного типа, например 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 p;

std::atomic a;

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.

Перейти на страницу:

Похожие книги