Последовательно согласованное упорядочение
Упорядочение по умолчанию называется последовательно согласованным , потому что оно предполагает, что поведение программы согласовано с простым последовательным взглядом на мир. Если все операции над экземплярами атомарных типов последовательно согласованы, то поведение многопоточной программы такое же, как если бы эти операции выполнялись в какой-то определенной последовательности в одном потоке. Это самое простое для понимания упорядочение доступа к памяти, потому оно и подразумевается по умолчанию: все потоки должны видеть один и тот же порядок операций. Таким образом, становится достаточно легко рассуждать о поведении программы, написанной с использованием атомарных переменных. Можно выписать все возможные последовательности операций, выполняемых разными потоками, отбросить несогласованные и проверить, что в остальных случаях программа ведет себя, как и ожидалось. Это также означает, что порядок операций нельзя изменять; если в каком-то потоке одна операция предшествует другой, то этот порядок должен быть виден всем остальным потокам.
С точки зрения синхронизации, последовательно согласованное сохранение синхронизируется-с последовательно согласованной операцией загрузки той же переменной, в которой читается сохраненное значение. Тем самым мы получаем одно ограничение на упорядочение операций в двух или более потоках. Однако этим последовательная согласованность не исчерпывается. Любая последовательно согласованная операция, выполненная после этой загрузки, должна быть видна всякому другому потоку в системе с последовательно согласованными атомарными операциями именно как следующая за загрузкой. Пример в листинге 5.4 демонстрирует это ограничение на упорядочение в действии. Однако это ограничение не распространяется на потоки, в которых для атомарных операций задано ослабленное упорядочение — они по-прежнему могут видеть операции в другом порядке. Поэтому, чтобы получить пользу от последовательного согласования операций, его надо использовать во всех потоках.
Но за простоту понимания приходится платить. На машине со слабым упорядочением и большим количеством процессоров может наблюдаться заметное снижение производительности, потому что для поддержания согласованной последовательности операций, возможно, придётся часто выполнять дорогостоящие операции синхронизации процессоров. Вместе с тем следует отметить, что некоторые архитектуры процессоров (в частности, такие распространенные, как x86 и x86-64) обеспечивают последовательную согласованность с относительно низкими издержками, так что если вас волнует влияние последовательно согласованного упорядочения на производительность, ознакомьтесь с документацией но конкретному процессору.
В следующем листинге последовательная согласованность демонстрируется на примере. Операции загрузки и сохранения переменных x
и y
явно помечены признаком memory_order_seq_cst
, хотя его можно было бы и опустить, так как он подразумевается по умолчанию.
Листинг 5.4.Из последовательной согласованности вытекает полная упорядоченность
#include
#include
#include
std::atomic x, y;
std::atomic z;
void write_x() {
x.store(true, std::memory_order_seq_cst); ←
(1)
}
void write_y() {
y.store(true, std::memory_order_seq_cst); ←
(2)
}
void read_x_then_y() {
while (!x.load(std::memory_order_seq_cst));←
(3)
if (y.load(std::memory_order_seq_cst))
++z;
}
void read_y_then_x() {
while (!y.load(std::memory_order_seq_cst));←
(4)
if (x.load(std::memory_order_seq_cst))
++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); ←
(5)
}
Утверждение assert
(5)не может сработать, потому что первым должно произойти сохранение x
(1)или сохранение y
(2), пусть даже точно не сказано, какое именно. Если загрузка y
в функции read_x_then_y
(3)возвращает false
, то сохранение x
должно было произойти раньше сохранения y
, и в таком случае загрузка x
в read_y_then_x
(4)должна вернуть true
, потому что наличие цикла while
гарантирует, что в этой точке у
равно true
. Поскольку семантика memory_order_seq_cst
требует полного упорядочения всех операций, помеченных признаком memory_order_seq_cst
, то существует подразумеваемое отношение порядка между операцией загрузки y
, которая возвращает false
(3), и операцией сохранения y
(1). Чтобы имело место единственное полное упорядочение в случае, когда некоторый поток сначала видит x==true
, затем y==false
, необходимо, чтобы при таком упорядочении сохранение x
происходило раньше сохранения y
.
Читать дальше