Теперь представьте, что имеется целый ряд боксов, в каждом из которых сидит по человеку с блокнотом и телефоном. Это всё наши атомарные переменные. У каждой переменной свой порядок модификации (список значений в блокноте), по между ними нет никакой связи. Если каждый звонящий (вы, Карл, Анна, Дэйв и Фред) представляет поток, то именно такая картина наблюдается, когда все операции работают в режиме memory_order_relaxed
. К человеку, сидящему в боксе, можно обращаться и с другими просьбами, например: «запиши это число и скажи мне, что находится в конце списка» ( exchange
) или «запиши это число, если число в конце списка равно тому , в противном случае скажи мне, что я должен был бы предположить» ( compare_exchange_strong
), но общий принцип при этом не изменяется.
Применив эту метафору к программе в листинге 5.5, можно сказать, что write_x_then_y
означает, что некто позвонил человеку в боксе x
, попросил его записать true
, а потом позвонил человеку в боксе y
и попросил его записать true
. Поток, выполняющий функцию read_y_then_x
, раз за разом звонит человеку в боксе y
и спрашивает значение, пока не услышит true
, после чего звонит человеку в боксе x
и спрашивает значение у него. Человек в боксе x
не обязан сообщать вам какое-то конкретное значение из своего списка и с полным правом может назвать false
.
Из-за этого с ослабленными атомарными операциями трудно иметь дело. Чтобы они были полезны для межпоточной синхронизации, их нужно сочетать с атомарными операциями, работающими в режиме с более строгой семантикой упорядочения. Я настоятельно рекомендую вообще избегать ослабленных атомарных операций, если без них можно обойтись, а, если никак нельзя, то использовать крайне осторожно. Учитывая, насколько интуитивно неочевидные результаты получились в листинге 5.5 при наличии всего двух потоков и двух переменных, нетрудно представить себе сложности, с которыми придется столкнуться, когда потоков и переменных станет больше.
Один из способов организовать дополнительную синхронизацию, не прибегая к последовательной согласованности, — воспользоваться упорядочением захват-освобождение.
Упорядочение захват-освобождение
Упорядочение захват-освобождение — шаг от ослабленного упорядочения в сторону большего порядка; полной упорядоченности операций еще нет, но какая-то синхронизация уже возможна. При такой модели атомарные операции загрузки являются операциями захвата ( memory_order_acquire
), атомарные операции сохранения — операциями освобождения ( memory_order_release
), а атомарные операции чтения-модификации-записи (например, fetch_add()
или exchange()
) — операциями захвата , освобождения или того и другого ( memory_order_acq_rel
). Синхронизация попарная — между потоком, выполнившим захват, и потоком, выполнившим освобождение. Операция освобождения синхронизируется-с операцией захвата, которая читает записанное значение. Это означает, что различные потоки могут видеть операции в разном порядке, но возможны все-таки не любые порядки. В следующем листинге показала программа из листинга 5.4, переработанная под семантику захвата-освобождения вместо семантики последовательной согласованности.
Листинг 5.7.Из семантики захвата-освобождения не вытекает полная упорядоченность
#include
#include
#include
std::atomic x, y;
std::atomic z;
void write_x() {
x.store(true, std::memory_order_release);
}
void write_y() {
y.store(true, std::memory_order_release);
}
void read_x_then_y() {
while (!x.load(std::memory_order_acquire));
if (y.load(std::memory_order_acquire)) ←
(1)
++z;
}
void read_y_then_x() {
while (!y.load(std::memory_order_acquire));
if (x.load(std::memory_order_acquire)) ←
(2)
++z;
}
int main() {
x = false;
y = false;
z = 0;
std::thread a(write_x);
std::thread b(write_y);
std::thread с(read_x_then_y);
std::thread d(read_y_then_x);
a.join();
b.join();
c.join();
d.join();
assert(z.load() != 0); ←
(3)
}
В данном случае утверждение (3) может сработать (как и в случае ослабленного упорядочения), потому что обе операции загрузки — x
(2)и y
(1)могут прочитать значение false
. Запись в переменные x
и y
производится из разных потоков, но упорядоченность между освобождением и захватом в одном потоке никак не отражается на операциях в других потоках.
Читать дальше