5.3.5. Барьеры

Библиотека атомарных операций была бы неполна без набора барьеров. Это операции, которые налагают ограничения на порядок доступа к памяти без модификации данных. Обычно они используются в сочетании с атомарными операциями, помеченными признаком memory_order_relaxed. Барьеры — это глобальные операции, они влияют на упорядочение других атомарных операций в том потоке, где устанавливается барьер. Своим названием барьеры обязаны тому, что устанавливают в коде границу, которую некоторые операции не могут пересечь. В разделе 5.3.3 мы говорили, что компилятор или сам процессор вправе изменять порядок ослабленных операций над различными переменными. Барьеры ограничивают эту свободу и вводят отношения происходит-раньше и синхронизируется-с, которых до этого не было.

В следующем листинге демонстрируется добавление барьера между двумя атомарными операциями в каждом потоке из листинга 5.5.

Листинг 5.12. Ослабленные операции можно упорядочить с помощью барьеров

#include <atomic>

#include <thread>

#include <assert.h>

std::atomic<bool> x, y;

std::atomic<int> z;

void write_x_then_y() {

 x.store(true, std::memory_order_relaxed);           ← (1)

 std::atomic_thread_fence(std::memory_order_release);← (2)

 y.store(true, std::memory_order_relaxed);           ← (3)

}

void read_y_then_x() {

 while (!y.load(std::memory_order_relaxed));         ← (4)

 std::atomic_thread_fence(std::memory_order_acquire);← (5)

 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.