По-перше, ви повинні навчитися мислити як юрист з мов.
Специфікація C ++ не посилається на якийсь конкретний компілятор, операційну систему чи процесор. Він посилається на абстрактну машину, яка є узагальненням фактичних систем. У світі юристів з мов завданням програміста є написання коду для абстрактної машини; завдання компілятора - актуалізувати цей код на конкретній машині. Швидко кодуючи специфікацію, ви можете бути впевнені, що ваш код буде збиратись та працювати без модифікацій у будь-якій системі із сумісним компілятором C ++, будь то сьогодні чи через 50 років.
Абстрактна машина в специфікації C ++ 98 / C ++ 03 є принципово однопоточною. Тому неможливо написати багатопотоковий код C ++, який є "повністю портативним" стосовно специфікації. Специфікація навіть нічого не говорить про атомність навантажень і сховищ пам'яті або про порядок, в якому можуть відбуватися навантаження і зберігання, не маючи на увазі речі, як мутекси.
Звичайно, ви можете написати багатопотоковий код на практиці для конкретних конкретних систем - наприклад, pthreads або Windows. Але не існує стандартного способу написання багатопотокового коду для C ++ 98 / C ++ 03.
Абстрактна машина в C ++ 11 є багатопотоковою конструкцією. Він також має чітко визначену модель пам'яті ; це означає, що компілятор може, а що не може робити, коли йдеться про доступ до пам'яті.
Розглянемо наступний приклад, коли пара глобальних змінних одночасно доступна двома потоками:
Global
int x, y;
Thread 1 Thread 2
x = 17; cout << y << " ";
y = 37; cout << x << endl;
Що може вийти з теми 2?
Під C ++ 98 / C ++ 03 це навіть не визначена поведінка; саме питання є безглуздим, оскільки стандарт не споглядає нічого, що називається "нитка".
Під C ++ 11 результат - Невизначена поведінка, тому що навантаження та магазини взагалі не повинні бути атомними. Що може не здатися великим покращенням ... І само по собі це не так.
Але за допомогою C ++ 11 ви можете написати це:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17); cout << y.load() << " ";
y.store(37); cout << x.load() << endl;
Зараз речі стають набагато цікавішими. Перш за все, тут визначається поведінка . Нитка 2 тепер може друкувати 0 0
(якщо вона запускається до теми 1), 37 17
(якщо вона працює після теми 1), або 0 17
(якщо вона працює після того, як тема 1 призначається x, але раніше вона призначається y).
Що він не може надрукувати 37 0
, тому що за замовчуванням для атомних навантажень / сховищ у С ++ 11 є наведення послідовності послідовності . Це просто означає, що всі навантаження та сховища повинні бути «ніби», вони відбувалися в тому порядку, коли ви їх написали в межах кожного потоку, тоді як операції між потоками можуть бути переплетені, як не подобається системі. Так поведінка за умовчанням Атомікса забезпечує як неподільність і впорядкованість для навантажень і магазинів.
Тепер на сучасному процесорі забезпечення послідовності консистенції може бути дорогим. Зокрема, компілятор, ймовірно, видає повномасштабні бар'єри пам'яті між кожним доступом тут. Але якщо ваш алгоритм може терпіти навантаження та магазини поза замовленням; тобто, якщо вона вимагає атомності, але не впорядковує; тобто, якщо він може терпіти 37 0
як вихід з цієї програми, ви можете написати це:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_relaxed); cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed); cout << x.load(memory_order_relaxed) << endl;
Чим сучасніший процесор, тим більше шансів на те, що це буде швидше, ніж у попередньому прикладі.
Нарешті, якщо вам просто потрібно підтримувати певні вантажі та магазини в порядку, ви можете написати:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_release); cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release); cout << x.load(memory_order_acquire) << endl;
Це повертає нас до замовлених вантажів і магазинів - тому 37 0
вже не можливий вихід - але це робиться з мінімальними витратами. (У цьому тривіальному прикладі результат такий самий, як повномасштабна послідовна консистенція; у більшій програмі цього не було б.)
Звичайно, якщо єдиними виходами, які ви хочете бачити, є 0 0
або 37 17
, ви можете просто обернути мютекс навколо початкового коду. Але якщо ви читали це далеко, я думаю, ви вже знаєте, як це працює, і ця відповідь вже довша, ніж я задумав :-).
Отже, підсумок. Мутекси чудові, а C ++ 11 їх стандартизує. Але іноді з міркувань продуктивності ви хочете примітивів нижчого рівня (наприклад, класичну схему блокування з подвійною перевіркою ). Новий стандарт надає гаджети високого рівня, такі як мутекси та змінні стану, а також надає гаджети низького рівня, такі як атомні типи та різні аромати бар'єру пам'яті. Отже, тепер ви можете писати складні, високопродуктивні паралельні процедури повністю в межах мови, визначеної стандартом, і ви можете бути впевнені, що ваш код буде компілюватися та працювати без змін як на сьогоднішніх системах, так і на завтрашніх.
Хоча, якщо чесно, якщо ви не експерт і працюєте над серйозним кодом низького рівня, ви, ймовірно, повинні дотримуватися мітекси та змінних умов. Це те, що я маю намір зробити.
Детальніше про цей матеріал читайте у цій публікації в блозі .