Понимание std:: atomic:: compare exchange weak() в C++11


bool compare_exchange_weak (T& expected, T val, ..);

compare_exchange_weak() является одним из примитивов compare-exchange, представленных в 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++ Concurrency In Action" Энтони говорит:

//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 там в состоянии цикла? Это там предотвратить то, что все потоки могут голодать и не прогрессировать в течение некоторого времени?

Edit: (последний вопрос)

на платформах, на которых не существует ни одной аппаратной инструкции 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):

когда сравнение и обмен находятся в цикле, слабая версия даст лучшая производительность на некоторых платформах.

но как было проанализировано выше, две версии в цикле должны давать одинаковую / аналогичную производительность. Чего мне не хватает?

4 69

4 ответа:

зачем делать обмен в цикле?

как правило, вы хотите, чтобы ваша работа была сделана, прежде чем двигаться дальше, таким образом, вы ставите compare_exchange_weak в цикл, так что он пытается обмениваться, пока это не удастся (т. е. возвращает true).

обратите внимание, что также compare_exchange_strong часто используется в цикле. Он не терпит неудачу из-за ложного сбоя, но он терпит неудачу из-за одновременной записи.

зачем использовать weak вместо strong?

довольно легко: паразитный отказ не делает случаются часто, так что это не большой хит производительности. В constrast, терпеть такой отказ позволяет для гораздо более эффективной реализации weak версия (по сравнению с strong) на некоторых платформах: strong необходимо всегда проверять наличие ложного отказа и маскировать его. Это дорого стоит.

таким образом, weak используется, потому что это намного быстрее, чем strong на некоторых платформах

когда следует использовать weak и когда strong?

в ссылка состояния подсказывает, когда использовать weak и когда использовать strong:

когда сравнение и обмен находятся в цикле, слабая версия даст лучшая производительность на некоторых платформах. Когда слабое сравнение-и-обмен потребуется петля, а сильный не будет, сильный-это предпочтительный.

таким образом, ответ кажется довольно простым для запоминания: Если вам придется вводить цикл только из-за ложного отказа, не делайте этого; используйте strong. Если у вас есть цикл в любом случае, то используйте weak.

почему

это зависит от ситуации и ее желаемой семантики, но обычно она не нужна для корректности. Опуская его, мы получим очень похожую семантику. Только в том случае, когда другой поток может сбросить значение false, семантика может стать немного другой (но я не могу найти значимый пример, где вы хотели бы этого). Тони Д. комментарий для подробного объяснения.

это просто быстрый путь, когда другое нить пишет true: тогда мы прерываем вместо того, чтобы пытаться написать true снова.

о вашем последнем вопросе

но как было проанализировано выше, две версии в цикле должны давать одинаковую / аналогичную производительность. Чего мне не хватает?

С Википедия:

реальные реализации LL / SC не всегда успешны, если их нет одновременное обновление соответствующей области памяти. Каких-либо исключительных события между двумя операциями, например переключение контекста, другой load-link, или даже (на многих платформах) другой груз или магазин операция, приведет к тому, что хранилище-условное ложно выйдет из строя. Более старый реализация будет выполнена, если есть какие-либо обновления, распространяемые шина памяти.

Итак, LL/SC не будет ложно на контекст переключатель, например. Теперь сильная версия принесет свой "собственный небольшой цикл", чтобы обнаружить этот ложный сбой и замаскировать его, попытавшись снова. Обратите внимание, что этот собственный цикл также сложнее, чем обычный цикл CAS, поскольку он должен различать ложную ошибку (и маскировать ее) и сбой из-за параллельного доступа (что приводит к возврату со значением false). Слабая версия не имеет такого собственного цикла.

после явного цикла в обоих примерах, это просто не необходимо иметь небольшой цикл для сильной версии. Следовательно, в Примере с strong версия, проверка на отказ выполняется дважды; один раз compare_exchange_strong (что более сложно, так как он должен различать ложные сбои и параллельные доступы) и один раз по вашему циклу. Эта дорогая проверка не нужна и причина, почему weak здесь будет быстрее.

также обратите внимание, что ваш аргумент (LL / SC) просто один возможность реализовать это. Есть и другие платформы, которые имеют даже различные наборы инструкций. Кроме того (и что более важно), обратите внимание, что std::atomic должны поддерживать все операции на все возможные типы данных, поэтому даже если вы объявите десятимиллионную структуру байтов, вы можете использовать compare_exchange об этом. Даже когда на процессоре, который имеет CAS, вы не можете CAS десять миллионов байт, поэтому компилятор будет генерировать другие инструкции (вероятно, lock acquire, а затем неатомное сравнение и обмен, а затем освобождение блокировки). Теперь, подумайте о сколько всего может произойти при обмене десятью миллионами байт. Таким образом, хотя ложная ошибка может быть очень редкой для 8-байтовых обменов, в этом случае она может быть более распространенной.

Итак, в двух словах, C++ дает вам две семантики, "лучшее усилие" (weak) и "я сделаю это наверняка, независимо от того, сколько плохих вещей может произойти между ними" (strong). Как они реализуются на различных типах данных и платформах-это совершенно другая тема. Не привязывайте свою ментальную модель к реализация на вашей конкретной платформе; стандартная библиотека предназначена для работы с большим количеством архитектур, чем вы могли бы знать. Единственный общий вывод, который мы можем сделать, заключается в том, что гарантировать успех обычно сложнее (и, следовательно, может потребовать дополнительной работы), чем просто пытаться и оставлять место для возможного провала.

почему это должно быть в цикле в почти все использует ?

потому что, если вы не зацикливаетесь, и он не работает ложно, ваша программа не сделала ничего полезного - вы не обновили атомарный объект, и вы не знаете, какое его текущее значение (исправление: см. комментарий ниже от Cameron). Если вызов не делает ничего полезного, какой смысл делать это?

означает ли это, что мы будем цикл, когда он не работает из-за паразитных неудачи?

да.

если это так, почему мы беспокоимся использовать compare_exchange_weak() и написать цикл сами? Мы можем просто использовать compare_exchange_strong (), который я думаю, должен избавиться от ложных сбоев для нас. Каковы общие случаи использования compare_exchange_weak ()?

на некоторых архитектурах compare_exchange_weak является более эффективным, и паразитные сбои должны быть довольно редкими, поэтому можно было бы написать более эффективные алгоритмы с использованием слабая форма и петля.

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

почему !expected там в состоянии цикла?

значение могло бы получить установите значение true другим потоком, поэтому вы не хотите продолжать цикл, пытаясь установить его.

Edit:

но как было проанализировано выше, две версии в цикле должны давать одинаковую / аналогичную производительность. Чего мне не хватает?

конечно, очевидно, что на платформах, где возможен паразитный отказ, возможна реализация 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));

реальный пример для нескольких потоков, чтобы добавить элемент в один связанный список одновременно. Каждый поток сначала загружает указатель head, выделяет новый узел и добавляет head к этому новому узлу. Наконец, он пытается поменять новый узел с головой.

Другим примером является реализация мьютекса с помощью std::atomic<bool>. Не более одного потока может войти в критический раздел одновременно, в зависимости от того, какой поток первый набор current до true и выходим из цикла.

Типичный Образец B

это на самом деле шаблон, упомянутый в книге Энтони. В противоположность шаблону а,вы хотите, чтобы атомарная переменная была обновлена один раз, но вам все равно, кто это делает. пока он не обновлен, попробуйте еще раз. Это обычно используется с булевыми переменными. Например, вам нужно реализовать триггер для государственной машины, чтобы двигаться дальше. Какая нить тянет курок-это не обращающий внимания.

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 требует:

реализации должны гарантировать, что слабое сравнение и обмен операции не всегда возвращают false, если ни одна атомная объект имеет значение отличное от ожидаемого или есть параллельные модификации атомарного объекта.

почему мы беспокоимся использовать compare_exchange_weak () и написать цикл сами? Мы можем просто использовать compare_exchange_strong().

это зависит.

Случай 1: Когда оба нужно использовать внутри цикла. в C++11 говорит:

когда сравнение и обмен находится в цикле, слабая версия даст лучшая производительность на некоторых платформах.

на x86 (по крайней мере в настоящее время. Возможно, он прибегнет к аналогичной схеме, как LL/SC один день для производительности когда вводится больше ядер), слабая и сильная версии по существу одинаковы, потому что они оба сводятся к одной инструкции cmpxchg. На некоторых других платформах, где compare_exchange_XXX() не реализован атомарно (здесь не существует ни одного аппаратного примитива), слабая версия внутри цикла может выиграть битву, потому что сильная должна будет обрабатывать ложные сбои и повторять попытку соответственно.

а,

редко, мы предпочитаем compare_exchange_strong() over compare_exchange_weak() даже в петле. Например, когда есть много вещей, чтобы сделать между атомной переменной загружается и вычисленное новое значение обменивается (см. function() выше). Если атомарная переменная сама по себе не изменяется часто, нам не нужно повторять дорогостоящий расчет для каждого ложного сбоя. Вместо этого, мы можем надеяться, что compare_exchange_strong() "впитывают" такие неудачи и мы только повторите расчет, когда он не выполняется из-за изменения реального значения.

случай 2: Когда толькоcompare_exchange_weak()нужно использовать внутри цикла. в C++11 также говорит:

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

обычно это происходит, когда вы зацикливаетесь только для устранения ложных сбоев из слабой версии. Повторите попытку, пока exchange не будет успешно или не удалось из-за параллельной записи.

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));
}

теперь есть две причины, по которым цикл может выполняться более одного раза.

  1. кто-то еще изменил переменную, пока я делал свой левый сдвиг. Результаты моих вычислений не должны применяться к атомарному переменная, потому что она будет эффективно стирать эту чужую запись.
  2. мой процессор рыгнул, и слабый CAS ложно провалился.

честно говоря, мне все равно, какой из них. Левый сдвиг достаточно быстр, чтобы я мог просто сделать это снова, даже если сбой был ложным.

что это меньше быстрый, однако, это дополнительный код, который сильный CAS должен обернуть вокруг слабого CAS, чтобы быть сильным. Этот код не делает много, когда слабый CAS преуспевает... но когда это не удается, сильный CAS должен сделать некоторую детективную работу, чтобы определить, был ли это случай 1 или Случай 2. Эта детективная работа принимает форму второй петли, фактически внутри моей собственной петли. Два вложенных цикла. Представьте, что ваш учитель алгоритмов смотрит на вас прямо сейчас.

и как я уже упоминал ранее, меня не волнует результат этой детективной работы! В любом случае я собираюсь переделать CAS. Поэтому использование сильного CAS не приносит мне ровно ничего, и теряет меня немного но измеримое количество эффективности.

другими словами, слабый CAS используется для реализации атомарных операций обновления. Сильный CAS используется, когда вы заботитесь о результате CAS.