Рис. 5.7.Последовательность освобождений для операций с очередью из листинга 5.11
В цепочке может быть сколько угодно звеньев, но при условии, что все они являются операциями чтения-модификации-записи, как fetch_sub()
, операция store()
синхронизируется-с каждым звеном, помеченным признаком memory_order_acquire
. В данном примере все звенья одинаковы и являются операциями захвата, но это вполне могли бы быть разные операции с разной семантикой упорядочения доступа к памяти.
Хотя большая часть отношений синхронизации проистекает из семантики упорядочения доступа к памяти, применённой к операциям над атомарными переменными, существует возможность задать дополнительные ограничения на упорядочение с помощью барьеров (fence).
Библиотека атомарных операций была бы неполна без набора барьеров. Это операции, которые налагают ограничения на порядок доступа к памяти без модификации данных. Обычно они используются в сочетании с атомарными операциями, помеченными признаком memory_order_relaxed
. Барьеры — это глобальные операции, они влияют на упорядочение других атомарных операций в том потоке, где устанавливается барьер. Своим названием барьеры обязаны тому, что устанавливают в коде границу, которую некоторые операции не могут пересечь. В разделе 5.3.3 мы говорили, что компилятор или сам процессор вправе изменять порядок ослабленных операций над различными переменными. Барьеры ограничивают эту свободу и вводят отношения происходит-раньше и синхронизируется-с, которых до этого не было.
В следующем листинге демонстрируется добавление барьера между двумя атомарными операциями в каждом потоке из листинга 5.5.
Листинг 5.12.Ослабленные операции можно упорядочить с помощью барьеров
#include
#include
#include
std::atomic x, y;
std::atomic 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
:
Читать дальше