Во-первых, вы должны научиться мыслить как юрист по языку.
Спецификация C ++ не содержит ссылок на какой-либо конкретный компилятор, операционную систему или процессор.Он ссылается на абстрактную машину , которая является обобщением реальных систем.В мире Language Lawyer работа программиста заключается в написании кода для абстрактной машины;работа компилятора состоит в том, чтобы актуализировать этот код на конкретной машине.Жестко кодируя спецификацию, вы можете быть уверены, что ваш код будет компилироваться и выполняться без изменений в любой системе с совместимым компилятором 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
, потому что режим по умолчанию для атомарных загрузок / хранилищ в C ++ 11 - обеспечить последовательную согласованность .Это просто означает, что все загрузки и хранилища должны быть «такими, как если бы» происходили в том порядке, в котором вы их записали в каждом потоке, а операции между потоками могут чередоваться, как нравится системе.Таким образом, стандартное поведение атома обеспечивает как атомарность , так и упорядочение для нагрузок и хранилищ.
Теперь на современном CPU обеспечение последовательной согласованности может быть дорогостоящим.В частности, компилятор, вероятно, будет создавать полноценные барьеры памяти между каждым доступом здесь.Но если ваш алгоритм может терпеть неупорядоченные загрузки и хранения;то есть, если это требует атомарности, но не упорядоченности;то есть, если он может допустить 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 их стандартизирует. Но иногда по соображениям производительности вам нужны низкоуровневые примитивы (например, классический дважды проверенный шаблон блокировки ). Новый стандарт предоставляет высокоуровневые гаджеты, такие как мьютексы и условные переменные, а также низкоуровневые гаджеты, такие как атомарные типы и различные варианты барьера памяти. Так что теперь вы можете писать сложные высокопроизводительные параллельные процедуры полностью на языке, указанном в стандарте, и вы можете быть уверены, что ваш код будет компилироваться и выполняться без изменений как в современных системах, так и в завтрашних.
Хотя, честно говоря, если вы не являетесь экспертом и не работаете над серьезным низкоуровневым кодом, вам, вероятно, следует придерживаться мьютексов и условных переменных. Вот что я намерен сделать.
Подробнее об этом см. в этом блоге .