Для рассуждений о транзитивном упорядочении нужны по меньшей мере три потока. Первый модифицирует какие-то разделяемые переменные и выполняет операцию сохранения с освобождением в одну из них. Второй читает переменную, записанную операцией сохранения с освобождением, с помощью операции загрузки с захватом и выполняет сохранение с освобождением во вторую разделяемую переменную. Наконец, третий поток выполняет операцию загрузки с захватом для второй разделяемой переменной. При условии, что операции загрузки с захватом видят значения, записанные операциями сохранения с освобождением, и тем самым поддерживают отношения синхронизируется-с, третий поток может прочитать значения других переменных, сохраненные первым потоком, даже если промежуточный поток к ним не обращался. Этот сценарий иллюстрируется в следующем листинге.
Листинг 5.9. Транзитивная синхронизация с помощью упорядочения захват-освобождение
std::atomic
std::atomic
void thread_1() {
data[0].store(42, std::memory_order_relaxed);
data[1].store(97, std::memory_order_relaxed);
data[2].store(17, std::memory_order_relaxed);
data[3].store(-141, std::memory_order_relaxed);
data[4].store(2003, std::memory_order_relaxed);←┐Установить
sync1.store(true, std::memory_order_release); (1)sync1
}
void thread_2() (2)Цикл до
{ │установки
while (!sync1.load(std::memory_order_acquire));←┘sync1
sync2.store(true, std::memory_order_release); ←┐Установить
} (3) sync2
void thread_3() (4)Цикл до
{ │установки
while (!sync2.load(std::memory_order_acquire));←┘sync2
assert(data[0].load(std::memory_order_relaxed) == 42);
assert(data[1].load(std::memory_order_relaxed) == 97);
assert(data[2].load(std::memory_order_relaxed) == 17);
assert(data[3].load(std::memory_order_relaxed) == -141);
assert(data[4].load(std::memory_order_relaxed) == 2003);
}
Хотя поток thread_2 обращается только к переменным sync1 (2) и sync2 (3), этого достаточно для синхронизации между thread_1 и thread_3 и, стало быть, гарантии несрабатывания утверждений assert. Прежде всего, операции сохранения в элементы массива data в потоке thread_1 происходят-раньше сохранения sync1 (1), потому что они связаны отношением расположено-перед в одном потоке. Поскольку операция загрузки sync1 (2) находится внутри цикла while, она в конце концов увидит значение, сохраненное в thread_1 и, значит, образует вторую половину пары освобождение-захват. Поэтому сохранение sync1 происходит-раньше последней загрузки sync1 в цикле while. Эта операция загрузки расположена-перед (и, значит, происходит-раньше) операцией сохранения sync2 (3), которая образует пару освобождение-захват вместе с последней операцией загрузки в цикле while в потоке thread_3 (4). Таким образом, сохранение sync2 (3) происходит-раньше загрузки (4), которая происходит-раньше загрузок data. В силу транзитивности отношения происходит-раньше всю эту цепочку можно соединить: операции сохранения data происходят-раньше операций сохранения sync1 (1), которые происходят-раньше загрузки sync1 (2), которая происходит-раньше сохранения sync2 (3), которая происходит-раньше загрузки sync2 (4), которая происходит-раньше загрузок data. Следовательно, операции сохранения data в потоке thread_1 происходят-раньше операций загрузки data в потоке thread_3, и утверждения assert сработать не могут.
В этом случае можно было бы объединить sync1 и sync2 в одну переменную, воспользовавшись операцией чтения-модификации-записи с семантикой memory_order_acq_rel в потоке thread_2. Один из вариантов — использовать функцию compare_exchange_strong(), гарантирующую, что значение будет обновлено только после того, как поток thread_2 увидит результат сохранения в потоке thread_1: