Я сам пытаюсь ответить на этот вопрос, просмотрев различные онлайн-ресурсы (например, этот и этот ), стандарт 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()
также будет помещен в цикл, что возвращает нас к предыдущему случаю.