bool compare_exchange_weak (T& expected, T val, ..);
compare_exchange_weak()
является одним из примитивов сравнения-обмена, представленных в C ++ 11. Он слабый в том смысле, что возвращает false, даже если значение объекта равно expected
. Это происходит из-за ложного сбоя на некоторых платформах, где для его реализации используется последовательность инструкций (вместо одной, как на x86). На таких платформах переключение контекста, перезагрузка того же адреса (или строки кэша) другим потоком и т. Д. Могут привести к сбою примитива. Дело в том, что операция spurious
не выполняется из-за значения объекта (не равного expected
). Вместо этого, это проблемы со сроками.
Но меня озадачивает то, что сказано в стандарте C ++ 11 (ISO / IEC 14882),
29.6.5. Последствием ложного отказа является то, что почти все случаи использования слабого сравнения и обмена будут замкнутыми.
Почему он должен быть замкнутым почти во всех случаях ? Означает ли это, что мы будем зацикливаться, когда он выйдет из строя из-за ложных отказов? Если это так, то зачем мы сами используем compare_exchange_weak()
и пишем цикл? Мы можем просто использовать то, compare_exchange_strong()
что, я думаю, должно избавить нас от ложных сбоев. Каковы распространенные варианты использования compare_exchange_weak()
?
Другой вопрос по теме. В своей книге «Параллелизм в C ++ в действии» Энтони говорит:
//Because compare_exchange_weak() can fail spuriously, it must typically
//be used in a loop:
bool expected=false;
extern atomic<bool> b; // set somewhere else
while(!b.compare_exchange_weak(expected,true) && !expected);
//In this case, you keep looping as long as expected is still false,
//indicating that the compare_exchange_weak() call failed spuriously.
Почему !expected
есть условие цикла? Есть ли это для предотвращения того, чтобы все потоки могли голодать и не продвигаться в течение некоторого времени?
Изменить: (последний вопрос)
На платформах, на которых не существует единой аппаратной инструкции CAS, как слабая, так и сильная версии реализованы с использованием LL / SC (например, ARM, PowerPC и т. Д.). Так есть ли разница между следующими двумя циклами? Зачем, если есть? (На мой взгляд, они должны иметь аналогичную производительность.)
// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_weak(..))
{ .. }
// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_strong(..))
{ .. }
Я подхожу к этому последнему вопросу, который вы, ребята, все упомянули, что, возможно, внутри цикла есть разница в производительности. Это также упоминается в стандарте C ++ 11 (ISO / IEC 14882):
Когда в цикле сравнения и обмена, слабая версия дает лучшую производительность на некоторых платформах.
Но, как было проанализировано выше, две версии в цикле должны давать одинаковую / похожую производительность. Что мне не хватает?
источник
Ответы:
Зачем делать обмен в цикле?
Обычно вы хотите, чтобы ваша работа была выполнена до того, как вы двинетесь дальше, поэтому вы включаете
compare_exchange_weak
цикл, чтобы он пытался обмениваться до тех пор, пока не завершится успешно (то есть не вернетсяtrue
).Обратите внимание, что также
compare_exchange_strong
часто используется в цикле. Он не дает сбоя из-за ложного сбоя, но сбой происходит из-за одновременной записи.Зачем использовать
weak
вместоstrong
?Довольно просто: ложные отказы случаются не часто, поэтому это не сильно снижает производительность. Напротив, терпимость к такому отказу позволяет гораздо более эффективно реализовать
weak
версию (по сравнению сstrong
) на некоторых платформах:strong
всегда необходимо проверять ложный отказ и маскировать его. Это дорого.Таким образом,
weak
используется, потому что он намного быстрее, чемstrong
на некоторых платформах.Когда
weak
и когда использоватьstrong
?В справочнике указаны подсказки, когда использовать,
weak
а когда использоватьstrong
:Так что ответ кажется довольно простым для запоминания: если вам придется ввести цикл только из-за ложного сбоя, не делайте этого; использовать
strong
. Если у вас все равно есть петля, используйтеweak
.Почему
!expected
в примереЭто зависит от ситуации и ее желаемой семантики, но обычно не требуется для корректности. Если его опустить, семантика будет очень похожа. Только в том случае, если другой поток может сбросить значение до
false
, семантика может немного измениться (но я не могу найти значимого примера, в котором вы бы этого хотели). См. Комментарий Тони Д. для подробного объяснения.Это просто быстрый способ, когда другой поток пишет
true
: Затем мы прерываем работу вместо того, чтобы пытаться написатьtrue
снова.О вашем последнем вопросе
Из Википедии :
Так, например, LL / SC ложно откажет при переключении контекста. Теперь сильная версия принесет свой «небольшой цикл», чтобы обнаружить этот ложный отказ и замаскировать его, повторив попытку. Обратите внимание, что этот собственный цикл также более сложен, чем обычный цикл CAS, поскольку он должен различать ложный отказ (и маскировать его) и сбой из-за одновременного доступа (что приводит к возврату со значением
false
). В слабой версии такого собственного шлейфа нет.Поскольку в обоих примерах вы предоставляете явный цикл, для сильной версии просто не нужно иметь маленький цикл. Следовательно, в примере с
strong
версией проверка на сбой выполняется дважды; один разcompare_exchange_strong
(что более сложно, поскольку он должен различать ложный сбой и одновременный доступ) и один раз вашим циклом. В этой дорогой проверке нет необходимости, и поэтому здесьweak
будет быстрее.Также обратите внимание, что ваш аргумент (LL / SC) - всего лишь одна возможность реализовать это. Есть другие платформы, которые имеют даже разные наборы команд. Вдобавок (и что более важно) обратите внимание, что
std::atomic
должны поддерживаться все операции для всех возможных типов данных , поэтому, даже если вы объявите структуру в десять миллионов байт, вы можете использоватьcompare_exchange
ее. Даже на ЦП, у которого есть CAS, вы не можете CAS десять миллионов байтов, поэтому компилятор сгенерирует другие инструкции (возможно, получение блокировки, за которым следует неатомарное сравнение и свопинг, за которым следует снятие блокировки). А теперь подумайте, сколько всего может произойти при обмене десятью миллионами байтов. Таким образом, хотя ложная ошибка может быть очень редкой для 8-байтовых обменов, в этом случае она может быть более распространенной.Вкратце, C ++ дает вам две семантики: one (
weak
) и «я сделаю это обязательно, независимо от того, сколько плохих вещей может случиться между ними» one (strong
). Как это реализовано на разных типах данных и на разных платформах - это совершенно другая тема. Не привязывайте свою ментальную модель к реализации на вашей конкретной платформе; стандартная библиотека предназначена для работы с большим количеством архитектур, чем вы можете себе представить. Единственный общий вывод, который мы можем сделать, заключается в том, что гарантировать успех обычно труднее (и, следовательно, может потребоваться дополнительная работа), чем просто попытаться оставить место для возможной неудачи.источник
b
уже находитtrue
, тогда - сexpected
сейчасtrue
- без&& !expected
этого зацикливается и пробует другой (глупый) обмен,true
иtrue
который вполне может "успешно", тривиально выходя изwhile
цикла, но может показать осмысленно другое поведение , еслиb
бы в то время изменился назадfalse
, и в этом случае цикл будет продолжаться , и , возможно , в конечном счете , установитьb
true
еще раз , прежде чем нарушать.Потому что, если вы не зацикливаетесь и он ложно терпит неудачу, ваша программа не сделала ничего полезного - вы не обновили атомарный объект и не знаете, каково его текущее значение (исправление: см. Комментарий Кэмерона ниже). Если звонок не делает ничего полезного, какой смысл это делать?
Да.
На некоторых архитектурах
compare_exchange_weak
это более эффективно, и ложные сбои должны быть довольно редкими, поэтому можно было бы написать более эффективные алгоритмы, используя слабую форму и цикл.В целом, вероятно, лучше использовать сильную версию, если вашему алгоритму не требуется цикл, так как вам не нужно беспокоиться о ложных сбоях. Если ему все равно нужно выполнить цикл даже для сильной версии (а многие алгоритмы в любом случае нуждаются в цикле), то использование слабой формы может быть более эффективным на некоторых платформах.
Значение могло быть установлено
true
другим потоком, поэтому вы не хотите продолжать цикл, пытаясь установить его.Редактировать:
Конечно, очевидно, что на платформах, где возможен ложный отказ, реализация
compare_exchange_strong
должна быть более сложной, чтобы проверить на ложный отказ и повторить попытку.Слабая форма просто возвращается при ложном сбое, она не пытается повторить попытку.
источник
you don't know what its current value is
в 1-й точке, когда происходит ложный отказ, не должно ли текущее значение равняться ожидаемому значению в этот момент? В противном случае это был бы настоящий провал.while(!compare_exchange_weak(..))
иwhile(!compare_exchange_strong(..))
?Я сам пытаюсь ответить на этот вопрос, просмотрев различные онлайн-ресурсы (например, этот и этот ), стандарт 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; // !expected: if expected is set to true by another thread, it's done! // Otherwise, it fails spuriously and we should try again. 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:
Почему мы не используем 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; // !expected: if it fails spuriously, we should try again. while (!current.compare_exchange_weak(expected, true) && !expected);
В лучшем случае это изобретает колеса заново и работает так же, как
compare_exchange_strong()
. Хуже? Этот подход не позволяет в полной мере использовать все преимущества машин, которые обеспечивают аппаратное непостоянное сравнение и обмен .Наконец, если вы выполняете цикл для других вещей (например, см. «Типичный шаблон A» выше), то есть хороший шанс, что он
compare_exchange_strong()
также будет помещен в цикл, что возвращает нас к предыдущему случаю.источник
Хорошо, мне нужна функция, которая выполняет атомарный сдвиг влево. У моего процессора нет встроенной операции для этого, а в стандартной библиотеке нет функции для этого, поэтому похоже, что я пишу свою собственную. Вот оно:
void atomicLeftShift(std::atomic<int>* var, int shiftBy) { do { int oldVal = std::atomic_load(var); int newVal = oldVal << shiftBy; } while(!std::compare_exchange_weak(oldVal, newVal)); }
Теперь есть две причины, по которым цикл может выполняться более одного раза.
Честно говоря, мне все равно, какой именно. Переключение влево происходит достаточно быстро, и я могу просто сделать это снова, даже если отказ был ложным.
Однако менее быстрым является дополнительный код, который требуется сильному CAS, чтобы обернуть слабый CAS, чтобы быть сильным. Этот код мало что делает, когда слабый CAS работает успешно ... но когда он терпит неудачу, сильному CAS нужно проделать некоторую детективную работу, чтобы определить, был ли это Случай 1 или Случай 2. Эта детективная работа принимает форму второго цикла, эффективно внутри моего собственного цикла. Две вложенные петли. Представьте, что ваш учитель алгоритмов смотрит на вас прямо сейчас.
И как я уже упоминал, меня не волнует результат этой детективной работы! В любом случае я собираюсь переделать CAS. Таким образом, использование сильного CAS абсолютно ничего не дает мне и теряет небольшую, но измеримую эффективность.
Другими словами, слабый CAS используется для реализации операций атомарного обновления. Strong CAS используется, когда вам важен результат CAS.
источник
Я думаю, что в большинстве приведенных выше ответов «ложный сбой» рассматривается как некоторая проблема, компромисс между производительностью и правильностью.
Видно, что слабая версия в большинстве случаев быстрее, но в случае ложного сбоя она становится медленнее. А сильная версия - это версия, у которой нет возможности ложного отказа, но она почти всегда работает медленнее.
Для меня главное различие в том, как эти две версии решают проблему ABA:
слабая версия будет успешной только в том случае, если никто не коснулся строки кеша между загрузкой и сохранением, поэтому она на 100% обнаружит проблему ABA.
сильная версия потерпит неудачу только в случае неудачного сравнения, поэтому она не обнаружит проблему ABA без дополнительных мер.
Итак, теоретически, если вы используете слабую версию в слабоупорядоченной архитектуре, вам не нужен механизм обнаружения ABA, и реализация будет намного проще, что даст лучшую производительность.
Но на x86 (строго упорядоченная архитектура) слабая и сильная версии одинаковы, и обе страдают от проблемы ABA.
Таким образом, если вы пишете полностью кросс-платформенный алгоритм, вам все равно необходимо решить проблему ABA, поэтому использование слабой версии не дает выигрыша в производительности, но снижает производительность за обработку ложных сбоев.
В заключение - по причинам переносимости и производительности сильная версия всегда является лучшим или равным вариантом.
Слабая версия может быть лучшим вариантом, только если она позволяет полностью пропустить контрмеры ABA или ваш алгоритм не заботится о ABA.
источник