В этом случае можно было бы объединить sync1
и sync2
в одну переменную, воспользовавшись операцией чтения-модификации-записи с семантикой memory_order_acq_rel
в потоке thread_2
. Один из вариантов — использовать функцию compare_exchange_strong()
, гарантирующую, что значение будет обновлено только после того, как поток thread_2
увидит результат сохранения в потоке thread_1
:
std::atomic sync(0);
void thread_1() {
// ...
sync.store(1, std::memory_order_release);
}
void thread_2() {
int expected = 1;
while (!sync.compare_exchange_strong(expected, 2,
std::memory_order_acq_rel))
expected = 1;
}
void thread_3() {
while(sync.load(std::memory_order_acquire) < 2);
// ...
}
При использовании операций чтения-модификации-записи важно выбрать нужную семантику. В данном случае нам нужна одновременно семантика захвата и освобождения, поэтому подойдет memory_order_acq_rel
, но можно было бы применить другие виды упорядочения. Операция fetch_sub
с семантикой memory_order_acquire
не синхронизируется ни с чем, хотя и сохраняет значение, потому что это не операция освобождения. Аналогично сохранение не может синхронизироваться-с операцией fetch_or
с семантикой memory_order_release
, потому что часть «чтение» fetch_or
не является операцией захвата. Операции чтения-модификации-записи с семантикой memory_order_acq_rel
ведут себя как операции захвата и освобождения одновременно, поэтому предшествующее сохранение может синхронизироваться-с такой операцией и с последующей загрузкой, как и обстоит дело в примере выше.
Если вы сочетаете операции захвата-освобождения с последовательно согласованными операциями, то последовательно согласованные операции загрузки ведут себя, как загрузки с семантикой захвата, а последовательно согласованные операции сохранения — как сохранения с семантикой освобождения. Последовательно согласованные операции чтения-модификации-записи ведут себя как операции, наделенные одновременно семантикой захвата и освобождения. Ослабленные операции так и остаются ослабленными, но связаны дополнительными отношениями синхронизируется-с и последующими отношениями происходит-раньше, наличие которых обусловлено семантикой захвата-освобождения.
Несмотря на интуитивно неочевидные результаты, всякий, кто использовал блокировки, вынужденно имел дело с вопросами упорядочения: блокировка мьютекса — это операция захвата, а его разблокировка — операция освобождения. Работая с мьютексами, вы на опыте узнаете, что при чтении значения необходимо захватывать тот же мьютекс, который захватывался при его записи. Точно так же обстоит дело и здесь — для обеспечения упорядочения операции захвата и освобождения должны применяться к одной и той же переменной. Если данные защищены мьютексом, то взаимно исключающая природа блокировки означает, что результат неотличим от того, который получился бы, если бы операции блокировки и разблокировки были последовательно согласованы. Аналогично, если для построения простой блокировки к атомарным переменным применяется упорядочение захват-освобождение, то с точки зрения программы, использующей такую блокировку, поведение кажется последовательно согласованным, хотя внутренние операции таковыми не являются.
Если для выполняемых в вашей программе атомарных операций не нужна строгость последовательно согласованного упорядочения, то попарная синхронизация с помощью упорядочения захват-освобождение может обеспечить синхронизацию со значительно меньшими издержками, чем необходимое для последовательно согласованных операций глобальное упорядочение. Ценой компромисса являются мысленные усилия, необходимые для того, чтобы удостовериться в том, что упорядочение работает правильно, а интуитивно неочевидное поведение нескольких потоков не вызывает проблем.
Зависимости по данным, упорядочение захват-освобождение и семантика memory_order_consume
Во введении к этому разделу я говорил, что семантика memory_order_consume
является частью модели упорядочения захват-освобождение, но из предшествующего описания она полностью выпала. Дело в том, что семантика memory_order_consume
особая: она связана с зависимостями по данным и позволяет учесть соответствующие нюансы в отношении межпоточно происходит-раньше , о котором шла речь в разделе 5.3.2.
С зависимостями по данным связаны два новых отношения: предшествует-по-зависимости (dependency-ordered-before) и переносит-зависимость-в (carries-a-dependency-to). Как и отношение расположено-перед, отношение переносит-зависимость-в применяется строго внутри одного потока и моделирует зависимость по данным между операциями — если результат операции А используется в качестве операнда операции В, то А переносит-зависимость-в В. Если результатом операции А является значение скалярного типа, например int
, то отношение применяется и тогда, когда результат А сохраняется в переменной, которая затем используется в качестве операнда В. Эта операция также транзитивна, то есть если А переносит-зависимость-в В и В переносит-зависимость-в С, то А переносит-зависимость-в С.
Читать дальше