Коли використовувати леткі з багатопоточною різьбою?


130

Якщо є два потоки, що мають доступ до глобальної змінної, то багато підручників кажуть, що робить цю змінну мінливою, щоб запобігти компілятору кешувати змінну в регістрі, і, таким чином, вона не оновлюється правильно. Однак два потоки, що мають доступ до спільної змінної, - це те, що вимагає захисту за допомогою mutex, чи не так? Але в такому випадку між блокуванням потоку та вивільненням мютексу код знаходиться у критичному розділі, де тільки цей один потік може отримати доступ до змінної, і в цьому випадку змінна не повинна бути мінливою?

Тож, у чому полягає корисність / цілі енергонезалежності в багатопотоковій програмі?


3
У деяких випадках ви не хочете / не потребуєте захисту мютекс.
Стефан Май

4
Іноді прекрасно мати стан перегонів, іноді це не так. Як ви використовуєте цю змінну?
Девід Геффернан

3
@David: Приклад, коли "добре" провести гонку, будь ласка?
Джон Дайблінг

6
@John Ось іде. Уявіть, у вас є робоча нитка, яка обробляє ряд завдань. Робоча нитка збільшує лічильник кожного разу, коли він закінчує завдання. Головний потік періодично читає цей лічильник та оновлює користувача новинами про прогрес. Поки лічильник правильно вирівняний, щоб уникнути розриву, немає необхідності синхронізувати доступ. Хоча є гонка, вона доброякісна.
Девід Геффернан

5
@John Обладнання, на якому працює цей код, гарантує, що вирівняні змінні не можуть страждати від розриву. Якщо працівник оновлює n до n + 1, коли читач читає, читачеві не важливо, отримують вони n або n + 1. Ніяких важливих рішень не приймається, оскільки воно використовується лише для звітування про хід розвитку.
Девід Геффернан

Відповіді:


167

Коротка та швидка відповідь : volatile(майже) марна для платформно-агностичного, багатопотокового програмування програм. Він не забезпечує жодної синхронізації, не створює огорожі пам’яті, а також не забезпечує порядок виконання операцій. Це не робить операції атомними. Це не робить ваш код магічною ниткою безпечним. volatileможе бути єдиним нерозуміним об'єктом у всіх C ++. Дивіться це , це та це для отримання додаткової інформації проvolatile

З іншого боку, volatileчи має це певне використання, яке може бути не таким очевидним. Це може бути використано багато за тим самим способом, який можна використати, constщоб допомогти компілятору показати вам, де ви можете помилитися в доступі до якогось спільного ресурсу незахищеним способом. Це використання розглядає Олександреску в цій статті . Однак в основному це використовується система типу C ++ таким чином, що часто розглядається як сприятливий характер і може викликати не визначене поведінку.

volatileбув спеціально призначений для використання під час взаємодії з обладнанням, нанесеним на карту пам'яті, обробниками сигналів та інструкцією машинного коду setjmp. Це робить volatileзастосовно безпосередньо до програмування на рівні системи, а не для звичайного програмування на рівні додатків.

Стандарт C ++ 2003 року не говорить про те, що volatileзастосовується будь-яка семантика придбання чи випуску на змінних. Насправді, Стандарт абсолютно мовчить з усіх питань багатопотокової роботи. Однак конкретні платформи застосовують семантику Acquire і Release на volatileзмінних.

[Оновлення для C ++ 11]

C ++ 11 Стандарту в даний час робить квитирование многопоточности безпосередньо в моделі пам'яті і lanuage і надає бібліотечні засоби для боротьби з ним в кроссплатформенную шляху. Однак семантика volatileдосі не змінилася. volatileвсе ще не є механізмом синхронізації. Bjarne Stroustrup говорить стільки ж у TCPPPL4E:

Не використовуйте volatileхіба що код низького рівня, який безпосередньо стосується обладнання.

Не припускайте, що volatileце особливе значення в моделі пам'яті. Це не. Це не - як у деяких пізніших мовах - механізм синхронізації. Щоб отримати синхронізацію, використовуйте atomic, a mutexабо a condition_variable.

[/ Закінчити оновлення]

Вище за все застосовується сама мова C ++, як це визначено Стандартом 2003 року (а тепер Стандарт 2011 року). Деякі конкретні платформи, однак, додають додаткових функціональних можливостей або обмежень для того, що volatileробить. Так , наприклад, в MSVC 2010 року (по крайней мере) Купувати і Release Семантика дійсно відносяться до певних операцій з volatileперемінним. Від MSDN :

Під час оптимізації компілятор повинен підтримувати впорядкування серед посилань на летючі об'єкти, а також посилань на інші глобальні об'єкти. Зокрема,

Запис на летючий об'єкт (volatile write) має семантику Release; посилання на глобальний або статичний об'єкт, що виникає перед записом на летючий об'єкт у послідовності інструкцій, відбуватиметься перед тим, як запитання запису в скомпільований двійковий файл.

Читання летючого об'єкта (летюче зчитування) має набути семантику; посилання на глобальний або статичний об'єкт, що виникає після зчитування летючої пам'яті в послідовності інструкцій, відбудеться після цього летучого зчитування у складеному двійковому файлі.

Тим НЕ менше, ви можете прийняти до відома той факт , що якщо ви будете слідувати наведеної вище посиланням, є деякі дебати в коментарях з приводу того, що не купувати / відпускання семантика фактично застосовується в даному випадку.


19
Частина мене хоче спростувати це через поблажливий тон відповіді та перший коментар. "непостійний мінливий" подібний до "ручного розподілу пам'яті марно". Якщо ви можете написати багатопотокову програму без volatileнеї, тому що ви стояли на плечах людей, які volatileреалізовували бібліотеки для нарізки.
Бен Джексон

19
@Ben тільки тому, що щось кидає виклик вашим переконанням, не робить це поблажливим
Девід Геффернан

38
@Ben: ні, читайте про те, що volatileнасправді робить C ++. Те, що сказав Джон, є правильним , кінець історії. Це не має нічого спільного з кодом програми проти кодом бібліотеки або "звичайними" проти "богоподібними всезнаючими програмістами" з цього питання. volatileє непотрібним і марним для синхронізації між потоками. Нитки бібліотек не можуть бути реалізовані з точки зору volatile; вона все одно повинна покладатися на деталі, що стосуються платформи, і коли ви покладаєтесь на ці, вам більше не потрібно volatile.
джельф

6
@jalf: "непостійний і непотрібний для синхронізації між потоками" (це те, що ви сказали) - це не те саме, що "непостійний є марним для багатопотокового програмування" (про що сказав Джон у відповіді). Ви на 100% правильні, але я не погоджуюся з Джоном (частково) - непостійний все ще можна використовувати для багатопотокового програмування (для дуже обмеженого набору завдань)

4
@GMan: Все корисне корисне лише за певного набору вимог чи умов. Летючий корисний для багатопотокового програмування за суворого набору умов (а в деяких випадках може бути навіть кращим (для певного визначення кращим), ніж альтернативи). Ви говорите "ігноруючи це, що і ..", але випадок, коли мінливий корисний для багатопотокового читання, нічого не ігнорує. Ви склали те, чого я ніколи не стверджував. Так, корисність летучих речовин обмежена, але вона існує - але ми можемо погодитися, що це НЕ корисно для синхронізації.

31

(Примітка редактора: в C ++ 11 volatileне є правильним інструментом для цієї роботи, і все ще є UB-перегони даних. Використовуйте std::atomic<bool>з std::memory_order_relaxedнавантаженнями / сховищами, щоб зробити це без UB. У реальних реалізаціях він буде компілюватися в той самий asm, що і volatileя. Я додав відповідь з більш детальною інформацією, а також усунення помилок у коментарях, що слабко упорядкована пам'ять може бути проблемою для цього випадку використання: усі реальні процесори мають когерентну спільну пам’ять, тому volatileвони працюватимуть для цього в реальних реалізаціях C ++. не роби це.

Деякий обговорення в коментарях , здається, говорити про інших споживчих випадках , коли вам буде необхідно що - то більш сильне , ніж розслаблених Атомікс. Ця відповідь вже вказує на те, що volatileви не замовляєте.)


Летючий час від часу корисний з наступної причини: цей код:

/* global */ bool flag = false;

while (!flag) {}

оптимізовано gcc до:

if (!flag) { while (true) {} }

Що, очевидно, неправильно, якщо прапор написаний іншим потоком. Зауважте, що без такої оптимізації механізм синхронізації, ймовірно, працює (залежно від іншого коду, можливо, знадобляться певні бар'єри пам’яті) - немає необхідності в мютекс у 1 виробника - 1 сценарію споживача.

В іншому випадку, мінливе ключове слово є надто дивним, щоб його можна було використовувати - воно не забезпечує жодного впорядкування пам'яті, гарантує wrt як енергонезалежний, так і енергонезалежний доступ і не забезпечує атомних операцій - тобто ви не отримуєте допомоги від компілятора з летючим ключовим словом, крім відключеного кешування регістрів .


4
Якщо я пригадую, атомний C ++ 0x призначений для того, щоб зробити належним чином те, що багато людей вважають (неправильно), робиться мінливим.
Девід Геффернан

13
volatileне запобігає переупорядкуванню доступу до пам'яті. volatileДоступ не буде переупорядкований відносно один одного, але вони не дають гарантії переупорядкування щодо не volatileоб’єктів, і, таким чином, вони в основному марні як прапори.
jalf

13
@Ben: Я думаю, ти це перевернув. Натовп "непостійний марний" покладається на простий факт, що летючий не захищає від переупорядкування , а це означає, що він є абсолютно марним для синхронізації. Інші підходи можуть бути настільки ж марними (як ви згадуєте, оптимізація коду за часом зв’язку може дозволити компілятору зазирнути до коду, який ви вважали, що компілятор трактуватиме як чорне поле), але це не виправляє недоліки volatile.
jalf

15
@jalf: Дивіться статтю Арка Робінсона (пов’язана з іншими місцями на цій сторінці), 10-й коментар (автор "Спуд"). В основному, переупорядкування не змінює логіку коду. Опублікований код використовує прапор для скасування завдання (замість того, щоб сигналізувати, що завдання виконано), тому не має значення, чи завдання скасовано до або після коду (наприклад: while (work_left) { do_piece_of_work(); if (cancel) break;}якщо скасування перепорядковане в циклі, Логіка все ще діє. У мене був фрагмент коду, який працював аналогічно: якщо основний потік хоче закінчитися, він встановлює прапор для інших потоків, але він не ...

15
... незалежно від того, чи інші нитки роблять зайві кілька ітерацій своїх циклів роботи, перш ніж вони закінчуються, доки це відбувається досить швидко після встановлення прапора. Звичайно, це ТІЛЬКЕ використання, яке я можу придумати, і його досить ніша (і може не працювати на платформах, де записування на мінливу змінну не робить зміни видимими для інших потоків, хоча принаймні x86 і x86-64 це твори). Я, звичайно, не радив би нікому насправді робити це без дуже вагомих причин, я просто говорю, що бланкова заява на зразок "мінливий НІКОЛИ не корисний у багатопотоковому коді" не є на 100% правильним.

15

У C ++ 11 зазвичай ніколи не використовуються volatileдля нарізування різьби, лише для MMIO

Але TL: DR, він працює "як би атомний" mo_relaxedна апаратному забезпеченні з когерентними кешами (тобто все); достатньо зупинити компілятори, які зберігають параметри в реєстрах. atomicне потрібні бар'єри пам’яті для створення атомності чи видимості між потоками, лише для того, щоб поточний потік чекав до / після операції, щоб створити впорядкування між доступом цього потоку до різних змінних. mo_relaxedніколи не потребує жодних бар'єрів, просто завантажуйте, зберігайте або обробляйте RMW.

Для рулонного свого власного Атомікса з volatile(і інлайн-асемблер для бар'єрів) в старі часи до C ++ 11 std::atomic, volatileбув тільки хорошим способом отримати деякі речі , щоб працювати . Але це залежало від безлічі припущень про те, як реалізовувались роботи і ніколи не гарантувалися жодним стандартом.

Наприклад, ядро ​​Linux все ще використовує власну ручну атоміку з volatile, але підтримує лише декілька конкретних реалізацій C (GNU C, clang та, можливо, ICC). Частково це пов’язано з розширеннями GNU C та синтаксисом та семантикою вбудованої ASM, а також тому, що це залежить від деяких припущень щодо роботи компіляторів.

Майже завжди неправильний вибір нових проектів; ви можете використовувати std::atomicstd::memory_order_relaxed), щоб отримати компілятор, щоб випромінювати той самий ефективний машинний код, що і ви volatile. std::atomicз mo_relaxedзастарілими volatileдля цілей різьблення. (за винятком можливо обійти помилки пропущеної оптимізації з atomic<double>деякими компіляторами .)

Внутрішня реалізація std::atomicосновних компіляторів (наприклад, gcc та clang) не використовує лише volatileвнутрішньо; компілятори безпосередньо піддають атомному навантаженню, накопиченню та вбудованому в RMW функції. (наприклад, вбудовані GNU C,__atomic які працюють на "звичайних" об'єктах.)


Леткі можна використовувати на практиці (але не робіть цього)

Це volatileбуло застосовано на практиці для таких речей, як exit_nowпрапор для всіх (?) Існуючих реалізацій C ++ на реальних процесорах, через те, як працюють центральні процесори (узгоджені кеші) та спільні припущення про те, як volatileслід працювати. Але не багато іншого, і не рекомендується. Мета цієї відповіді - пояснити, як реально працюють існуючі процесори та C ++. Якщо вам це не байдуже, все, що вам потрібно знати, - це те, що std::atomicзастарілі mo_relaxed застаріли volatileдля нанизування різьби.

(Стандарт ISO C ++ щодо нього досить розпливчастий, просто кажучи, що volatileдоступ слід оцінювати строго відповідно до правил абстрактної машини C ++, а не оптимізувати його. Враховуючи, що реальні реалізації використовують адресний простір пам'яті машини для моделювання адресного простору C ++, це означає, що volatileчитання та завдання повинні компілюватися для завантаження / зберігання інструкцій для доступу до представлення об'єктів у пам'яті.)


Як вказується інша відповідь, exit_nowпрапор - це простий випадок зв'язку між потоками, який не потребує синхронізації : не публікується, що вміст масиву готовий чи щось подібне. Просто магазин, який негайно помітили неоптимізоване завантаження в іншій нитці.

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

Без мінливих або атомних, правило , як-ніби, і припущення про відсутність перегону даних UB дозволяє компілятору оптимізувати його в асм, який перевіряє прапор лише один раз , перед тим як вводити (чи ні) нескінченний цикл. Саме це і відбувається в реальному житті для справжніх компіляторів. (І зазвичай оптимізуємо велику частину, do_stuffоскільки цикл ніколи не виходить, тому будь-який пізній код, який, можливо, використовував результат, недоступний, якщо ми введемо цикл).

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

Програма багатопотокової роботи, що застрягла в оптимізованому режимі, але працює нормально в -00, є прикладом (з описом виходу ASM GCC) того, як саме це відбувається з GCC на x86-64. Також програмування MCU - оптимізація C ++ O2 перервана під час циклу на електроніці.SE показує ще один приклад.

Ми, як правило, хочемо агресивних оптимізацій, які CSE та підйомник завантажує з циклів, в тому числі для глобальних змінних.

До C ++ 11 volatile bool exit_nowбув один із способів зробити цю роботу за призначенням (у звичайних C ++ реалізаціях). Але в C ++ 11 UB-перегони даних все ще застосовуються до цього, volatileтому він фактично не гарантується стандартом ISO для роботи скрізь, навіть якщо припускати, що HW є когерентними кешами.

Зауважте, що для ширших типів volatileне дає гарантії відсутності сліз. Я ігнорував це відмінність, boolоскільки це не проблема в звичайних реалізаціях. Але це також є частиною того, чому volatileвсе ще підлягає UB-перегоні даних, а не еквівалентні розслабленому атомному.

Зауважте, що "за призначенням" не означає, що нитка робить exit_nowочікування, поки інший потік фактично вийде. Або навіть, що він чекає, коли енергонезалежний exit_now=trueсховище стане навіть глобально видимим, перш ніж продовжувати наступні операції в цій темі. ( atomic<bool>за замовчуванням mo_seq_cstзмусить зачекати, перш ніж пізніше буде завантажено seq_cst. У багатьох ISA ви просто отримаєте повний бар'єр після магазину).

C ++ 11 забезпечує не-UB спосіб, який компілює той самий

А «продовжує працювати» або «вихід» Тепер прапор слід використовувати std::atomic<bool> flagзmo_relaxed

Використання

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

дасть вам таку саму зону (без дорогих інструкцій щодо бар'єру), від якої ви отримаєте volatile flag.

Крім того, що не рветься, atomicтакож дає можливість зберігати в одному потоці та завантажувати в інший без UB, тому компілятор не може підняти навантаження з циклу. (Припущення про відсутність перегонів з передачею даних - це те, що дозволяє робити агресивні оптимізації для неатомних енергонезалежних об'єктів.) Ця особливість atomic<T>майже однакова, як volatileі для чистих навантажень та чистих сховищ.

atomic<T>також здійснюйте +=операції з атомною РМЗ (значно дорожче, ніж атомний навантаження в тимчасовий, експлуатуйте, то окремий атомний склад. Якщо ви не хочете атомної RMW, напишіть свій код з місцевим тимчасовим).

Отримавши seq_cstзамовлення за замовчуванням, яке ви отримаєте while(!flag), воно також додає гарантії замовлення wrt. неатомарний доступ і до інших атомних доступу.

(Теоретично стандарт ISO C ++ не виключає оптимізацію атоміки під час компіляції. Але на практиці компілятори цього не роблять, оскільки немає способу контролю, коли це не буде нормально. Є кілька випадків, коли навіть volatile atomic<T>не може бути Буде достатньо контролю над оптимізацією атомів, якщо компілятори зробили оптимізацію, так що зараз компілятори не дивляться. Дивіться, чому компілятори не зливають зайве std :: atomic пише? Зауважте, що wg21 / p0062 рекомендує не використовувати volatile atomicв поточному коді захист від оптимізації атомія.)


volatile насправді працює для цього на реальних процесорах (але все одно не використовуйте)

навіть із слабко упорядкованими моделями пам'яті (не-x86) . Але на самому ділі не використовувати його, використовувати atomic<T>з mo_relaxedзамість !! Суть цього розділу полягає у вирішенні помилкових уявлень про те, як працюють реальні процесори, а не для виправдання volatile. Якщо ви пишете безблочний код, ви, ймовірно, дбаєте про продуктивність. Розуміння кеш-пам'яті та витрат на взаємодію між потоками, як правило, важливо для хорошої роботи.

Реальні процесори мають когерентні кеші / спільну пам'ять: після того, як сховище з одного ядра стане глобально видимим, жодне інше ядро ​​не може завантажити чергове значення. (Див. Також Міфи Програмісти вірять у кеші процесора, які розповідають про летучі Java, еквівалентні C ++ atomic<T>з порядком пам'яті seq_cst.)

Коли я кажу навантаження , я маю на увазі інструкцію ASM, яка отримує доступ до пам'яті. Ось що volatileзабезпечує доступ, і це не те саме, що перетворення lvalue в rvalue неатомної / енергонезалежної змінної C ++. (наприклад, local_tmp = flagабо while(!flag)).

Єдине, що вам потрібно перемогти - це оптимізація часу компіляції, яка взагалі не завантажується після першої перевірки. Будь-яке завантаження + перевірка кожної ітерації є достатньою, без будь-якого замовлення. Без синхронізації між цією ниткою та основною ниткою не має сенсу говорити про те, коли саме відбулося зберігання, чи впорядкування завантаження wrt. інші операції в циклі. Тільки коли це видно в цій темі, це важливо. Коли ви бачите встановлений прапор exit_now, ви виходите. Затримка між ядрами на типовому X86 Xeon може бути приблизно як 40ns між окремими фізичними ядрами .


Теоретично: C ++ потоки на апаратному забезпеченні без узгоджених кешів

Я не бачу, що це може бути віддалено ефективним, просто чистий ISO C ++, не вимагаючи від програміста явних помилок у вихідному коді.

Теоретично у вас може бути реалізація C ++ на машині, яка не була такою, що вимагає явних флеш-сигналів, згенерованих компілятором, щоб зробити видимими інші потоки на інших ядрах . (Або для читання, щоб не використовувати копію, можливо, несвіжу). Стандарт C ++ не робить цього неможливим, але модель пам'яті C ++ розроблена так, щоб бути ефективною на когерентних машинах спільної пам'яті. Наприклад, стандарт C ++ навіть говорить про "узгодженість читання-читання", "когерентність читання-читання" тощо. Одна примітка в стандарті навіть вказує на підключення до обладнання:

http://eel.is/c++draft/intro.races#19

[Примітка: чотири попередні вимоги до узгодженості ефективно забороняють упорядкування атомних операцій компілятором на один об'єкт, навіть якщо обидві операції є навантаженими розслабленими. Це ефективно забезпечує гарантію узгодженості кешу, яку надає більшість апаратних засобів, доступних для атомних операцій C ++. - кінцева примітка]

Немає механізму, щоб releaseмагазин зберігав лише очищення себе та декілька вибраних діапазонів адрес: він повинен був би синхронізувати все, оскільки він не знав би, які інші потоки можуть хотіти читати, якби їхнє завантаження бачило цей випуск-сховище (утворюючи послідовність випуску, яка встановлює взаємозв'язок, що відбувається перед потоками, гарантуючи, що раніше неатомні операції, виконані потоком запису, тепер безпечні для читання. Якщо тільки це не запише далі після сховища релізів ...) Або компілятори мали б щоб бути по- справжньому розумним, щоб довести, що лише кілька кеш-рядків потрібно промити.

Пов’язано: моя відповідь на те, чи безпечно Mov + mfence на NUMA? детально розповідається про відсутність систем x86 без когерентної спільної пам'яті. Також пов’язано: Вантажі та магазини, що упорядковують ARM для отримання додаткових відомостей про вантажі / магазини в одному місці.

Є , я думаю, кластери з некогерентною спільною пам'яттю, але вони не є системами одного зображення. Кожен домен когерентності працює окремим ядром, тому ви не можете запускати нитки однієї програми C ++ через нього. Натомість ви запускаєте окремі екземпляри програми (у кожного з їх власним адресним простором: вказівники в одному екземплярі недійсні в іншому).

Щоб змусити їх спілкуватися один з одним за допомогою явних флешів, зазвичай ви використовуєте MPI або інший API для передачі повідомлень, щоб програма визначала, які діапазони адрес потрібно промивати.


Справжнє обладнання не працює std::threadчерез межі когерентності кешу:

Існують деякі асиметричні ARM-мікросхеми, що мають спільний фізичний адресний простір, але не керовані домени кешу. Тож не злагоджені. (наприклад, потік коментаря до ядра A8 та Cortex-M3, як TI Sitara AM335x).

Але різні ядра працювали б на цих ядрах, а не єдине зображення системи, яке могло б запускати потоки по обидва ядра. Мені невідомі будь-які C ++ реалізації, які виконують std::threadпотоки в ядрах процесора без узгоджених кешів.

Зокрема для ARM, GCC та кланг генерують код, припускаючи, що всі потоки працюють у тому самому внутрішньообмінному домені. Насправді йдеться в посібнику ISA ARMv7

Ця архітектура (ARMv7) написана з очікуванням, що всі процесори, що використовують одну операційну систему або гіпервізор, знаходяться в одній домі Внутрішньої спільної доступності

Таким чином, несумісна спільна пам'ять між окремими доменами є лише справою для явного використання специфічних для системи областей спільної пам'яті для зв'язку між різними процесами під різними ядрами.

Дивіться також цю дискусію в CoreCLR про використання кодового родуdmb ish (Внутрішній обмінний бар'єр) проти dmb sy(Система) бар'єрів пам'яті у цьому компіляторі.

Я стверджую, що жодна реалізація C ++ для інших інших ISA не std::threadперетинає ядра з некогерентними кешами. У мене немає доказів того, що такої реалізації не існує, але це здається дуже малоймовірним. Якщо ви не орієнтуєтесь на певну екзотичну частину HW, яка працює таким чином, ваше мислення щодо продуктивності повинно припускати MESI-подібність кеш-пам'яті між усіма потоками. ( atomic<T>Однак переважно використовувати способи, які гарантують правильність!)


Узгоджені кеші роблять його простим

Але для багатоядерної системи з когерентними кешами реалізація сховища випусків просто означає замовлення фіксації в кеш для сховищ цього потоку, не роблячи явного очищення. ( https://preshing.com/20120913/acquire-and-release-semantics/ та https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (А придбання-завантаження означає замовлення доступу до кешу в іншому ядрі).

Інструкція щодо бар'єру пам’яті просто блокує завантаження поточного потоку та / або зберігання поточного потоку до тих пір, поки буфер магазину не зливеться; це завжди відбувається якомога швидше самостійно. ( Чи гарантує бар'єр пам’яті те, що кохерентність кешу завершена? Вирішує це неправильне уявлення). Тож якщо вам не потрібно замовляти, просто підкажіть видимість в інших потоках, mo_relaxedце добре. (Так і є volatile, але не робіть цього.)

Див. Також C / C ++ 11 відображень для процесорів

Приємний факт: на x86 кожен магазин ASM є сховищем випусків, оскільки модель пам'яті x86 в основному є seq-cst плюс буфер зберігання (з переадресацією магазину).


Напівзалежно від повторного зберігання буфера, глобальної видимості та когерентності: C ++ 11 гарантує дуже мало. Більшість реальних ISA (крім PowerPC) гарантують, що всі потоки можуть узгодити порядок появи двох магазинів двома іншими потоками. (У формальній термінології з моделями пам'яті комп'ютерної архітектури вони "атомні з декількома копіями").

Ще одне неправильне уявлення полягає в тому, що інструкції щодо заповнення пам’яті для забору пам’яті потрібні, щоб промити буфер магазину для інших ядер, щоб взагалі побачити наші магазини . Насправді буфер магазину завжди намагається максимально швидко злити (скористатися кешем L1d), інакше він заповнить і зупинить виконання. Що повний бар'єр / огорожа робить - це затримка поточної нитки, поки буфер магазину не виснажений , тому наші наступні вантажі з’являються в глобальному порядку після наших попередніх магазинів.

(сильно впорядкована модель пам'яті ASM означає, що volatileна x86 може зблизити вас mo_acq_rel, за винятком того, що перезапис часу компіляції з неатомними змінними все ще може відбутися. Але більшість тих, хто не має x86, мають слабко упорядковані моделі пам'яті, volatileі relaxedприблизно так слабкий, як mo_relaxedдозволяє.)


Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

2
Прекрасна дописка. Це саме те, що я шукав (наводячи всі факти) замість бланкетного твердження, яке просто говорить "використовувати атомний замість летючого для єдиного загального загального булевого прапора".
bernie

2
@bernie: Я написав це після того, як засмутився неодноразовими твердженнями, що невикористання atomicможе призвести до різних потоків, що мають різні значення для однієї змінної в кеші . / facepalm. У кеші немає, у процесорах реєструє так (з неатомними змінними); Процесори використовують когерентний кеш. Я хотів би, щоб інші питання щодо SO не були повні пояснень щодо atomicпоширення помилкових уявлень про те, як працюють процесори. (Оскільки це корисно зрозуміти з причин продуктивності, а також допомагає пояснити, чому атомні правила ISO C ++ написані такими, якими вони є.)
Пітер Кордес,

-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

Одного разу інтерв'юер, який також вважав, що мінливий марний, посперечався зі мною, що оптимізація не спричинить жодних проблем, і він мав на увазі різні ядра, що мають окремі лінії кешу, і все таке (не дуже розумів, на що він саме має на увазі). Але цей фрагмент коду при компілюванні з -O3 на g ++ (g ++ -O3 thread.cpp -lpthread), він демонструє невизначену поведінку. В основному, якщо значення встановлюється перед тим, як перевірити, воно працює нормально, а якщо ні, то воно переходить у цикл, не намагаючись отримати значення (що насправді було змінено іншим потоком). В основному я вважаю, що значення checkValue потрапляє лише один раз у реєстр і ніколи не перевіряється знову під найвищим рівнем оптимізації. Якщо його встановлено до істинного перед початком, він працює добре, а якщо ні, він переходить у цикл. Будь ласка, виправте мене, якщо я помиляюся.


4
Що це стосується volatile? Так, цей код є UB - але це також і UB volatile.
Девід Шварц

-2

Вам потрібен непостійний і, можливо, блокування.

непостійний повідомляє оптимізатору, що значення може змінюватися асинхронно, таким чином

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

буде читати прапор кожного разу навколо циклу.

Якщо вимкнути оптимізацію або зробити кожну змінну нестабільною, програма поводитиметься однаково, але повільніше. непостійний просто означає: "Я знаю, що ви, можливо, щойно прочитали його і знаєте, що він говорить, але якщо я скажу, прочитайте його, тоді прочитайте його".

Блокування є частиною програми. Тож, до речі, якщо ви впроваджуєте семафори, то серед іншого вони повинні бути мінливими. (Не намагайтеся, важко. Можливо, знадобиться трохи асемблера або новий атомний матеріал, і це вже було зроблено.)


1
Але хіба це не той самий приклад в іншій відповіді, зайнятий чеканням, а отже, чогось слід уникати? Якщо це надуманий приклад, чи є приклади реального життя, які не надумані?
Девід Престон

7
@Chris: Зайняте очікування час від часу є хорошим рішенням. Зокрема, якщо ви очікуєте, що доведеться зачекати лише пару циклів годин, це несе набагато менше накладних витрат, ніж набагато більш важкий підхід підвішування нитки. Звичайно, як я вже згадував в інших коментарях, такі приклади, як цей, є помилковими, оскільки вони припускають, що читання / запис до прапора не буде перепорядковане стосовно коду, який він захищає, і така гарантія не надається, і так , volatileнасправді не корисний навіть у цьому випадку. Але напружене очікування - це час від часу корисна техніка.
jalf

3
@richard Так і ні. Перша половина правильна. Але це означає лише, що центральному процесору та компілятору заборонено переставляти мінливі змінні відносно один одного. Якщо я читаю змінну змінну A, а потім читаю мінливу змінну B, тоді компілятор повинен видавати код, який гарантовано (навіть із переупорядкуванням процесора), щоб прочитати A перед B. Але він не дає гарантій щодо всіх нестабільних доступів змінної . Вони можуть бути упорядковані навколо вашого непостійного читання / запису просто чудово. Тож якщо ви не зробите кожну змінну своєї програми мінливою, вона не дасть вам гарантії, яка вас цікавить
jalf

2
@ ctrl-alt-delor: Це не те, що volatileозначає "не переупорядкування". Ви сподіваєтесь, що це означає, що магазини стануть глобально видимими (для інших потоків) у програмному порядку. Ось що atomic<T>з memory_order_releaseабо seq_cstдає вам. Але volatile лише ви даєте гарантію відсутності впорядкування часу компіляції : кожен доступ з’явиться в асмі в програмному порядку. Корисно для драйвера пристрою. І корисно для взаємодії з обробником переривання, налагоджувачем або обробником сигналу на поточному ядрі / потоці, але не для взаємодії з іншими ядрами.
Пітер Кордес

1
volatileна практиці достатньо для перевірки keep_runningпрапора, як ви робите тут: реальні процесори завжди мають узгоджені кеші, які не потребують ручної промивки. Але немає ніяких причин рекомендувати volatileперестати atomic<T>з mo_relaxed; ти отримаєш таку саму зору.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.