Как эта метафора моделирует семантику захвата-освобождения? Взгляните на наш пример — и поймете. В самом начале поток а
вызывает функцию write_x_then_y
и говорит человеку в боксе x
: «Запиши true
, как часть пакета 1 от потока а
». Затем поток а
говорит человеку в боксе y
: «Запиши true
, как последнюю операцию записи в пакете 1 от потока а
». Тем временем поток b
выполняет функцию read_y_then_x
. Он раз за разом просит человека в боксе y
сообщить значение вместе с информацией о пакете, пока не услышит в ответ « true
». Возможно, спросить придется много раз, но в конце концов человек обязательно ответит « true
». Однако человек в боксе y
говорит не просто « true
», а еще добавляет: «Это последняя операция записи в пакете 1 от потока а
».
Далее поток b
просит человека в боксе x
назвать значение, но на это раз говорит: «Сообщи мне значение и, кстати, я знаю о пакете 1 от потока а
». Человек в боксе x ищет в своем списке последнее упоминание о пакете 1 от потока а
. Он находит единственное значение true
, которое стоит последним в списке, поэтому он обязан сообщить именно это значение, иначе нарушит правила игры.
Вспомнив определение отношения межпоточно происходит раньше в разделе 5.3.2, вы обнаружите, что одно из его существенных свойств — транзитивность: если А межпоточно происходит-раньше В и В межпоточно происходит-раньше С , то А межпоточно происходит-раньше С . Это означает, что упорядочение захват-освобождение можно использовать для синхронизации данных между несколькими потоками, даже если «промежуточные» потоки на самом деле не обращались к данным.
Транзитивная синхронизация с помощью упорядочения захват-освобождение
Для рассуждений о транзитивном упорядочении нужны по меньшей мере три потока. Первый модифицирует какие-то разделяемые переменные и выполняет операцию сохранения с освобождением в одну из них. Второй читает переменную, записанную операцией сохранения с освобождением, с помощью операции загрузки с захватом и выполняет сохранение с освобождением во вторую разделяемую переменную. Наконец, третий поток выполняет операцию загрузки с захватом для второй разделяемой переменной. При условии, что операции загрузки с захватом видят значения, записанные операциями сохранения с освобождением, и тем самым поддерживают отношения синхронизируется-с, третий поток может прочитать значения других переменных, сохраненные первым потоком, даже если промежуточный поток к ним не обращался. Этот сценарий иллюстрируется в следующем листинге.
Листинг 5.9.Транзитивная синхронизация с помощью упорядочения захват-освобождение
std::atomic data[5];
std::atomic sync1(false), sync2(false);
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
сработать не могут.
Читать дальше