Я намагаюся відповісти на це сам, переглянувши різні Інтернет-ресурси (наприклад, цей і цей ), стандарт C ++ 11, а також відповіді, наведені тут.
Пов'язані запитання об'єднуються (наприклад, " чому! Очікується? " Об'єднується з "навіщо ставити порівняння_мінна_слабка () у цикл? ") І відповіді даються відповідно.
Чому функція compare_exchange_weak () майже в усіх циклах повинна бути у циклі?
Типовий шаблон A
Вам потрібно досягти атомного оновлення на основі значення в атомній змінній. Помилка вказує на те, що змінна не оновлюється нашим бажаним значенням, і ми хочемо повторити спробу. Зауважте, що нас насправді не хвилює, чи не вдається це через одночасний запис чи помилковий збій. Але нам все одно, що саме ми вносимо ці зміни.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Реальний приклад полягає в тому, що декілька потоків одночасно додають елемент до списку з єдиним зв’язком. Кожен потік спочатку завантажує покажчик head, виділяє новий вузол і додає head до цього нового вузла. Нарешті, він намагається поміняти новий вузол головою.
Інший приклад - реалізація використання mutex std::atomic<bool>
. По більшій мірі один потік може увійти в критичну секцію в той час, в залежності від того, який потік першого набору , current
щоб true
і вийти з циклу.
Типовий шаблон B
Це насправді шаблон, згаданий у книзі Ентоні. На відміну від шаблону А, ви хочете, щоб атомна змінна була оновлена один раз, але вам все одно, хто це робить. Поки він не оновлюється, спробуйте ще раз. Зазвичай це використовується з логічними змінними. Наприклад, вам потрібно реалізувати тригер для стану машини, щоб рухатися далі. Яка нитка натискає на курок, незалежно.
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: Коли обидва потрібно використовувати всередині циклу. C ++ 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()
також буде введений у цикл, що поверне нас до попереднього випадку.