Почему бы не использовать std::memory_order_acq_rel

Недавно я изучаю код проекта cppcoro. И у меня есть вопрос.

https://github.com/lewissbaker/cppcoro/blob/master/lib/async_auto_reset_event.cpp#L218 https://github.com/lewissbaker/cppcoro/blob/master/lib/async_auto_reset_event.cpp#L284

if (waiter->m_refCount.fetch_sub(1, std::memory_order_release) == 1) // #218
{
    waiter->m_awaiter.resume();
}

используя запись memory_order_release в строке 218, m_refCount с использованием флага memory_order_acquire может правильно загрузить значение в строке 284. Это нормально. Но fetch_sub — это операция RMW. Чтобы правильно прочитать модификацию в строке 284, нужен ли также флаг memory_order_aquire? Поэтому мне интересно, почему m_refCount не использует memory_order_acq_rel в строке 218 и строке 284?

return m_refCount.fetch_sub(1, std::memory_order_acquire) != 1; // #284

Спасибо.


person breaker00    schedule 07.12.2020    source источник
comment
Порядок памяти не меняет атомарность рассматриваемого объекта, они только ослабляют правила «происходит до» для других объектов.   -  person Caleth    schedule 07.12.2020


Ответы (2)


Потому что это не то, как работают заказы памяти.

Мы добавляем барьер памяти к нашей атомарной операции, чтобы добиться двух вещей:

  1. чтобы предотвратить переупорядочение расслабленных атомарных операций и неатомарных операций.
  2. для синхронизации неатомарных данных между потоками

Я написал ответ здесь, что объясняет эти два момента более четко.

Когда сопрограмма приостанавливается в одном потоке и возобновляется в другом потоке, дополнительная синхронизация не требуется*, cppreference для сопрограмм говорит:

Обратите внимание: поскольку сопрограмма полностью приостанавливается перед входом в awaiter.await_suspend(), эта функция может свободно передавать дескриптор сопрограммы между потоками без дополнительной синхронизации.

Что касается переоформления? фактическая логика ( waiter->m_awaiter.resume();) окружена одним большим толстым оператором if. компилятор все равно не может переупорядочить возобновление перед fetch_sub, потому что тогда он игнорирует роль оператора if и нарушает логику кода.

Итак, нам не нужен никакой другой порядок памяти, кроме расслабления. Тот факт, что fetch_XXX является операцией RMW, ничего не значит — мы используем правильный порядок памяти для правильного варианта использования.

Если вам нравится cppcoro, попробуйте мою собственную библиотеку сопрограмм, concurrencpp.

*Более правильное утверждение: никакой дополнительной синхронизации не требуется, кроме синхронизации, необходимой для передачи coroutine_handle из одного потока в другой.

person David Haim    schedule 07.12.2020
comment
Я понял после прочтения этого текста, который вы пишете в приведенной выше ссылке: Использование атомарной переменной решает проблему - с использованием атомарности все потоки гарантируют чтение последнего записанного значения, даже если порядок памяти ослаблен. У меня есть некоторые недоразумения по поводу флага memory_order_relaxed. Раньше я думал, что атомарные переменные используют флаг memory_order_relaxed. Не гарантируется, что операция записи потока A будет вовремя обнаружена при чтении в потоке B. - person breaker00; 08.12.2020
comment
Прочитайте связанный ответ еще раз, это распространенное заблуждение. Опять же, атомарные переменные всегда потокобезопасны. MO используется для синхронизации неатомарных данных и предотвращения переупорядочения. атомарная переменная не должна иметь MO, чтобы быть потокобезопасной. - person David Haim; 08.12.2020
comment
эмм.. Но просто разговор о безопасности потоков не решает моих прежних сомнений. Я понимаю, что безопасность потока, обеспечиваемая атомарной переменной, заключается в том, что она не будет считывать промежуточное значение, которое изменяется. Это обновление значения в потоке A не может быть вовремя обнаружено потоком B, поэтому чтение старого значения из-за кеша или других подобных причин, я думаю, не проблема безопасности потока, а проблема видимости памяти. Поэтому я раньше думал, что видимость, которая действует немедленно, обеспечивается какой-то МО, но я не знал, что она обеспечивается атомарной переменной self. - person breaker00; 08.12.2020

Операции с самой атомарной переменной в любом случае не вызовут гонок данных.

std::memory_order_release означает, что все измененные кэшированные данные будут зафиксированы в общей памяти/ОЗУ. Операции по порядку памяти генерируют барьеры памяти, чтобы другие объекты могли быть правильно переданы в общую память или прочитаны из нее.

person ALX23z    schedule 07.12.2020
comment
Вы можете улучшить, говоря о когерентности кеша. - person Surt; 07.12.2020
comment
Что вы имеете в виду под оперативной памятью? Физические модули оперативной памяти? - person curiousguy; 21.12.2020