Що гарантується C ++ std :: atomic на рівні програміста?


9

Я прослухав і прочитав декілька статей, бесід та запитань про поточний потік std::atomic, і хотів би бути впевненим, що я це добре зрозумів. Оскільки я все ще трохи заплутаний у кеш-рядку запису видимості через можливі затримки протоколів когерентності кеш-пам'яті MESI (або похідних), зберігання буферів, недійсних черг тощо.

Я прочитав, що x86 має більш сильну модель пам'яті, і якщо затримка відключення кешу затримана, x86 може відновити розпочаті операції. Але мене зараз цікавить лише те, що я повинен вважати програмістом C ++ незалежно від платформи.

[T1: thread1 T2: thread2 V1: загальна атомна змінна]

Я розумію, що std :: atomic гарантує це,

(1) За змінною не відбувається перегонів даних (завдяки виключному доступу до рядка кешу).

(2) Залежно від того, якою пам’яткою пам’яті ми користуємось, вона гарантує (з бар’єрами), що буде послідовна послідовність (перед бар’єром, після бар’єру чи обох).

(3) Після атомного запису (V1) на T1 атомна RMW (V1) на T2 буде когерентною (її кеш-рядок буде оновлено записуваним значенням на T1).

Але як згадується праймер когерентності кешу ,

Слід сказати, що за замовчуванням завантаження можуть отримувати несвіжі дані (якщо відповідна запит щодо недійсності сидів у черзі недійсності)

Отже, чи правильно таке?

(4) std::atomicНЕ гарантує, що T2 не буде читати «черстве» значення на атомному зчитуванні (V) після атомного запису (V) на T1.

Питання, якщо (4) вірно: якщо атомний запис на T1 приводить до недійсності лінії кеша незалежно від затримки, чому T2 чекає, що вимкнення буде ефективним, коли атомна операція RMW не зчитується?

Питання, якщо (4) помиляється: тоді, коли нитка може прочитати значення "несвіже" та "це видно" у виконанні?

Я дуже ціную ваші відповіді

Оновлення 1

Тож, здається, тоді я помилявся (3). Уявіть таке перемежування для початкового V1 = 0:

T1: W(1)
T2:      R(0) M(++) W(1)

Навіть незважаючи на те, що RMW T2 гарантовано відбудеться повністю після W (1), в цьому випадку він все ще може прочитати «несвіже» значення (я помилявся). Відповідно до цього, атомний не гарантує повну когерентність кешу, лише послідовну послідовність.

Оновлення 2

(5) Тепер уявіть цей приклад (x = y = 0 і є атомним):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

згідно з тим, про що ми говорили, побачення "msg", що відображається на екрані, не дасть нам інформації, крім того, що T2 був виконаний після T1. Тож може статися будь-яке з наступних страт:

  • T1 <T3 <T2
  • T1 <T2 <T3 (де T3 бачить x = 1, але не y = 1)

це так?

(6) Якщо потік завжди може читати "несвіжі" значення, що буде, якби ми взяли типовий сценарій "опублікувати", але замість того, щоб сигналізувати про те, що деякі дані готові, ми робимо навпаки (видаляємо дані)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

де T2 все ще буде використовувати видалений ptr, поки не побачить, що is_enabled не відповідає дійсності.

(7) Крім того, той факт, що потоки можуть читати «несвіжі» значення, означає, що мьютекс не може бути реалізований лише з одним атомним правом без блокування? Для цього знадобиться синхронізуючий механізм між потоками. Чи потрібна буде атомна, що може бути заблокована?

Відповіді:


3
  1. Так, немає перегонів даних
  2. Так, за допомогою відповідних memory_orderзначень ви можете гарантувати послідовну послідовність
  3. Атомне зчитування-зміна-запис завжди відбуватиметься цілком до або повністю після атомного запису на ту саму змінну
  4. Так, T2 може прочитати чергове значення зі змінної після атомного запису на T1

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

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

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

Оновлення, наприклад

Якщо T1 пише, x=1а T2 робить x++, xспочатку 0, вибір з точки зору зберігання x:

  1. Спочатку запису T1, тому T1 пише x=1, потім T2 зчитує з x==1кроком до 2 і записує назад x=2як одну атомну операцію.

  2. Запис T1 - другий. T2 зчитує x==0, збільшує його до 1 і записує назад x=1як одну операцію, потім T1 пише x=1.

Однак за умови, що між цими двома потоками немає інших точок синхронізації, потоки можуть продовжувати операції, не заповнені пам'яттю.

Таким чином, T1 може видавати x=1, а потім продовжувати інші речі, навіть незважаючи на те, що T2 все ще буде читати x==0(і таким чином писати x=1).

Якщо є якісь інші точки синхронізації, то стане очевидним, який потік буде змінено xспочатку, оскільки ці точки синхронізації змусять замовити.

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

Оновлення 2

  1. Якщо ви використовуєте memory_order_seq_cst(за замовчуванням) для всіх атомних операцій, вам не потрібно турбуватися про подібні речі. З точки зору програми, якщо ви бачите "msg", тоді біг T1, потім T3, потім T2.

Якщо ви використовуєте інші замовлення пам'яті (особливо memory_order_relaxed), то у вашому коді можуть з’явитися інші сценарії.

  1. У цьому випадку у вас є помилка. Припустимо, is_enabledпрапор справжній, коли T2 потрапляє у свою whileпетлю, тому він вирішує запустити тіло. T1 тепер видаляє дані, а T2 потім затримує вказівник, який є звисаючим вказівником, і настає невизначена поведінка . Атомія не допомагає і не перешкоджає, окрім запобігання гонці даних на прапорі.

  2. Ви можете реалізувати mutex з однією атомною змінною.


Дякую @Anthony Wiliams за швидку відповідь. Я оновив своє запитання на прикладі того, як RMW читає "несвіже" значення. Дивлячись на цей приклад, що ви маєте на увазі під відносним упорядкуванням і що W (1) T2 буде видно перед будь-яким записом? Чи означає це, що коли T2 побачив зміни T1, він більше не буде читати W (1) T2?
Альберт Кальдас

Отже, якщо "Нитки завжди можуть читати несвіжі значення", це означає, що послідовність кешу ніколи не гарантується (принаймні на рівні програміста c ++). Невже ви можете поглянути на моє оновлення2?
Альберт Кальдас

Тепер я бачу, що мені слід було б приділити більше уваги мовним і апаратним моделям пам'яті, щоб повністю зрозуміти все це, це був предмет, який мені не вистачало. дуже дякую!
Альберт Кальдас

1

Щодо (3) - це залежить від використовуваного порядку пам'яті. Якщо обидва, магазин і операція RMW використовують std::memory_order_seq_cst, то обидві операції впорядковані певним чином - тобто, або зберігання відбувається перед RMW, або навпаки. Якщо магазин замовляється перед RMW, то гарантується, що операція RMW "бачить" значення, яке було збережено. Якщо магазин буде замовлений після RMW, він перезаписав би значення, записане операцією RMW.

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

У випадку, якщо ви хочете прочитати ще одну статтю, я можу посилатись вам на Моделі пам'яті для програмістів C / C ++ .


Дякую за статтю, я її ще не читав. Навіть якщо він досить старий, корисно було скласти мої ідеї.
Альберт Кальдас

1
Радий почути - ця стаття є дещо розширеною та переглянутою главою з моєї магістерської роботи. :-) Основна увага приділяється моделі пам’яті, що була представлена ​​C ++ 11; Я можу оновити його, щоб відобразити (невеликі) зміни, внесені в C ++ 14/17. Будь ласка, повідомте мене, якщо у вас є якісь коментарі чи пропозиції щодо вдосконалень!
mpoeter
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.