Я сам пытаюсь ответить на этот вопрос, просмотрев различные онлайн-ресурсы (например, этот и этот ), стандарт C ++ 11, а также ответы, приведенные здесь.
Связанные вопросы объединяются (например, « почему! Ожидается? » Объединяется с «зачем помещать compare_exchange_weak () в цикл? »), И даются соответствующие ответы.
Почему compare_exchange_weak () должен быть в цикле почти во всех случаях использования?
Типичный образец A
Вам нужно добиться атомарного обновления на основе значения атомарной переменной. Ошибка означает, что переменная не обновлена до желаемого значения, и мы хотим повторить попытку. Обратите внимание, что нас действительно не волнует, произойдет ли сбой из-за одновременной записи или ложного сбоя. Но мы все равно , что это нам , что сделать это изменение.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Реальный пример - несколько потоков одновременно добавляют элемент в односвязный список. Каждый поток сначала загружает указатель головы, выделяет новый узел и добавляет заголовок к этому новому узлу. Наконец, он пытается поменять местами новый узел с головой.
Другой пример - реализация мьютекса с использованием std::atomic<bool>. По большей мере один поток может войти в критическую секцию в то время, в зависимости от того, какой поток первого набора , currentчтобы trueи выйти из цикла.
Типичный образец B
Это на самом деле образец, упомянутый в книге Энтони. В отличие от шаблона A, вы хотите, чтобы атомарная переменная обновлялась один раз, но вам все равно, кто это делает. Пока он не обновлен, попробуйте еще раз. Обычно это используется с логическими переменными. Например, вам нужно реализовать триггер для движения конечного автомата. Независимо от того, какая нить нажимает на курок.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Обратите внимание, что обычно мы не можем использовать этот шаблон для реализации мьютекса. В противном случае в критической секции могут одновременно находиться несколько потоков.
Тем не менее, использование compare_exchange_weak()вне цикла должно быть редкостью . Напротив, есть случаи, когда используется сильная версия. Например,
bool criticalSection_tryEnter(lock)
{
bool flag = false;
return lock.compare_exchange_strong(flag, true);
}
compare_exchange_weak здесь не подходит, потому что, когда он возвращается из-за ложного сбоя, вероятно, что никто еще не занимает критическую секцию.
Голодающая нить?
Стоит упомянуть один момент: что произойдет, если ложные сбои будут продолжать происходить, что приведет к истощению потока? Теоретически это могло произойти на платформах, когда compare_exchange_XXX()реализовано как последовательность инструкций (например, LL / SC). Частый доступ к одной и той же строке кэша между LL и SC приведет к постоянным ложным сбоям. Более реалистичный пример связан с тупым планированием, при котором все параллельные потоки чередуются следующим образом.
Time
| thread 1 (LL)
| thread 2 (LL)
| thread 1 (compare, SC), fails spuriously due to thread 2's LL
| thread 1 (LL)
| thread 2 (compare, SC), fails spuriously due to thread 1's LL
| thread 2 (LL)
v ..
Это может случиться?
К счастью, это не произойдет вечно, благодаря тому, что требует C ++ 11:
Реализации должны гарантировать, что слабые операции сравнения и обмена не будут последовательно возвращать false, если только атомарный объект не имеет значение, отличное от ожидаемого, или если не происходят одновременные модификации атомарного объекта.
Почему мы не используем compare_exchange_weak () и сами пишем цикл? Мы можем просто использовать compare_exchange_strong ().
Это зависит.
Случай 1: Когда оба должны использоваться внутри цикла. С ++ 11 говорит:
Когда в цикле сравнения и обмена, слабая версия дает лучшую производительность на некоторых платформах.
На x86 (по крайней мере, в настоящее время. Возможно, однажды для повышения производительности он прибегнет к такой же схеме, как LL / SC, когда будет добавлено больше ядер), слабая и сильная версии по сути одинаковы, потому что обе сводятся к одной инструкции cmpxchg. На некоторых других платформах, где compare_exchange_XXX()это не реализовано атомарно (это означает, что не существует единого аппаратного примитива), слабая версия внутри цикла может выиграть битву, потому что сильная версия должна будет обрабатывать ложные сбои и соответственно повторять попытки.
Но,
в редких случаях мы можем предпочесть compare_exchange_strong()вариант compare_exchange_weak()даже в цикле. Например, когда есть много дел между загрузкой атомарной переменной и обменом вычисленного нового значения (см. function()Выше). Если сама атомарная переменная не меняется часто, нам не нужно повторять дорогостоящие вычисления для каждого ложного отказа. Вместо этого мы можем надеяться, что compare_exchange_strong()«поглотим» такие сбои, и мы повторяем расчет только тогда, когда он терпит неудачу из-за реального изменения значения.
Случай 2: когда compare_exchange_weak() нужно использовать только внутри цикла. С ++ 11 также говорит:
Когда для слабого сравнения и обмена требуется петля, а для сильного - нет, предпочтительнее сильное.
Обычно это случается, когда вы выполняете цикл только для устранения ложных отказов слабой версии. Вы повторяете попытку до тех пор, пока обмен не будет успешным или неудачным из-за одновременной записи.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
В лучшем случае это изобретает колеса заново и работает так же, как compare_exchange_strong(). Хуже? Этот подход не позволяет в полной мере использовать все преимущества машин, которые обеспечивают аппаратное непостоянное сравнение и обмен .
Наконец, если вы выполняете цикл для других вещей (например, см. «Типичный шаблон A» выше), то есть хороший шанс, что он compare_exchange_strong()также будет помещен в цикл, что возвращает нас к предыдущему случаю.