if (x.load(std::memory_order_relaxed)) ←(6)
++z;
}
int main() {
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); ←(7)
}
Барьер освобождения (2) синхронизируется-с барьером захвата (5), потому что операция загрузки y в точке (4) читает значение, сохраненное в точке (3). Это означает, что сохранение x (1) происходит-раньше загрузки x (6), поэтому прочитанное значение должно быть равно true, и утверждение (7) не сработает. Здесь мы наблюдаем разительное отличие от исходного случая без барьеров, когда сохранение и загрузка x не были упорядочены, и утверждение могло сработать. Отметим, что оба барьера обязательны: чтобы получить отношение синхронизируется-с необходимо освобождение в одном потоке и захват в другом.
В данном случае барьер освобождения (2) оказывает такой же эффект, как если бы операция сохранения y (3) была помечена признаком memory_order_release, а не memory_order_relaxed. Аналогично эффект от барьера захвата (5) такой же, как если бы операция загрузки y (4) была помечена признаком memory_order_acquire. Это общее свойство всех барьеров: если операция захвата видит результат сохранения, имевшего место после барьера освобождения, то барьер синхронизируется-с этой операцией захвата. Если же операция загрузки, имевшая место до барьера захвата, видит результат операции освобождения, то операция освобождения синхронизируется-с барьером захвата. Разумеется, можно поставить барьеры по обе стороны, как в примере выше, и в таком случае если загрузка, которая имела место до барьера захвата, видит значение, записанное операцией сохранения, имевшей место после барьера освобождения, то барьер освобождения синхронизируется-с барьером захвата.
Хотя барьерная синхронизация зависит от значений, прочитанных или записанных операциями до и после барьеров, важно отметить, что точкой синхронизации является сам барьер. Если взять функцию write_x_then_y из листинга 5.12 и перенести запись в x после барьера, как показано ниже, то уже не гарантируется, что условие в утверждение будет истинным, несмотря на то что запись в x предшествует записи в y:
void write_x_then_y() {
std::atomic_thread_fence(std::memory_order_release);
x.store(true, std::memory_order_relaxed);
y.store(true, std::memory_order_relaxed);
}
Эти две операции больше не разделены барьером и потому не упорядочены. Барьер обеспечивает упорядочение только тогда, когда находится x и сохранением y. Конечно, наличие или отсутствие барьера не влияет на упорядочения, обусловленные отношениями происходит-раньше, которые существуют благодаря другим атомарным операциям.
Данный пример, как и почти все остальные в этой главе, целиком построен на переменных атомарных типов. Однако реальная польза от применения атомарных операций для навязывания упорядочения проистекает из того, что они могут упорядочивать неатомарные операции и тем самым предотвращать неопределенное поведение из-за гонок за данными, как мы видели в листинге 5.2.
5.3.6. Упорядочение неатомарных операций с помощью атомарных
Если заменить тип переменной x в листинге 5.12 обычным неатомарным типом bool (как в листинге ниже), то гарантируется точно такое же поведение, как и раньше.
Листинг 5.13. Принудительное упорядочение неатомарных операций
#include
#include
#include
bool x = false; ←┐Теперь x — простая
std::atomicнеатомарная
std::atomicпеременная
void write_x_then_y() {(1) Сохранение x
x = true; ←┘перед барьером
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed);←┐Сохранение y
} (2) после барьера
void read_y_then_x() (3) Ждем, пока не
{ │увидим значение,
while (!y.load(std::memory_order_relaxed));←┘записанное в 2
std::atomic_thread_fence(std::memory_order_acquire);
if (x) ←┐Здесь будет прочитано
++z; (4) значение, записанное в 1
}