c++ - C ++ 11 представил стандартизованную модель памяти. Что это значит? И как это повлияет на программирование на С ++?




3 Answers

Во-первых, вы должны научиться думать, как юрист по языку.

Спецификация 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;

Что могло бы вывести Thread 2?

В C ++ 98 / C ++ 03 это даже не неопределенное поведение; сам вопрос бессмыслен, поскольку стандарт не рассматривает ничего, называемое «нитью».

В C ++ 11 результатом является Undefined Behavior, потому что нагрузки и магазины не обязательно должны быть атомарными вообще. Что может показаться не очень хорошим улучшением ... И само по себе это не так.

Но с C ++ 11 вы можете написать это:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Теперь все становится намного интереснее. Прежде всего, здесь определяется поведение. Thread 2 теперь может печатать 0 0 (если он работает до Thread 1), 37 17 (если он выполняется после Thread 1) или 0 17 (если он запускается после того, как Thread 1 назначает x, но до того, как он назначит y).

То, что он не может напечатать, равен 37 0 , потому что режим по умолчанию для атомных нагрузок / хранилищ в C ++ 11 заключается в обеспечении последовательной согласованности . Это означает, что все нагрузки и хранилища должны быть «как если бы», они произошли в том порядке, в котором вы их записывали в каждом потоке, тогда как операции между потоками могут чередоваться, но система нравится. Таким образом, поведение Atomics по умолчанию обеспечивает как атомарность, так и порядок загрузки и хранения.

Теперь, на современном процессоре, обеспечение последовательной согласованности может быть дорогостоящим. В частности, компилятор, вероятно, испускает полномасштабные барьеры памяти между каждым доступом здесь. Но если ваш алгоритм может терпеть неуправляемые нагрузки и магазины; т.е. если он требует атомарности, но не упорядочивает; т.е. если он может вынести 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 их стандартизирует. Но иногда по соображениям производительности вам нужны примитивы нижнего уровня (например, классический шаблон с двойной проверкой блокировки ). Новый стандарт обеспечивает высокоуровневые гаджеты, такие как мьютексы и переменные состояния, а также предоставляет низкоуровневые гаджеты, такие как атомные типы и различные варианты защиты памяти. Итак, теперь вы можете писать сложные, высокопроизводительные параллельные подпрограммы полностью на языке, указанном стандартом, и вы можете быть уверены, что ваш код будет компилироваться и работать без изменений как на сегодняшних системах, так и на завтрашнем.

Хотя, если быть откровенным, если вы не являетесь экспертом и не работаете над каким-то серьезным низкоуровневым кодом, вы, вероятно, должны придерживаться мьютексов и переменных условий. Это то, что я намереваюсь сделать.

Подробнее об этом см. В этом сообщении в блоге .

C ++ 11 представил стандартизованную модель памяти, но что именно это означает? И как это повлияет на программирование на С ++?

Эта статья ( Гэвин Кларк , цитирующая Херба Саттера ) говорит, что,

Модель памяти означает, что код C ++ теперь имеет стандартизованную библиотеку для вызова независимо от того, кто создал компилятор и на какой платформе он работает. Существует стандартный способ управления тем, как разные потоки разговаривают с памятью процессора.

«Когда вы говорите о разделении [кода] на разные ядра, которые находятся в стандарте, мы говорим о модели памяти. Мы собираемся ее оптимизировать, не нарушая следующих предположений, которые люди собираются сделать в коде», - сказал Саттер .

Ну, я могу запомнить этот и аналогичные абзацы, доступные в Интернете (так как у меня была моя собственная модель памяти с момента рождения: P), и я могу даже написать ответ на вопросы, заданные другими, но, честно говоря, я не совсем понимаю этот.

Итак, что я в основном хочу знать, программисты на C ++ раньше разрабатывали многопоточные приложения, поэтому как это важно, если это потоки POSIX или потоки Windows или потоки C ++ 11? Каковы преимущества? Я хочу понять детали низкого уровня.

Я также чувствую, что модель памяти C ++ 11 каким-то образом связана с поддержкой многопоточности C ++ 11, поскольку я часто вижу их вместе. Если да, то как именно? Почему они должны быть связаны?

Поскольку я не знаю, как работает внутренняя часть многопоточности и какая модель памяти в целом, пожалуйста, помогите мне понять эти понятия. :-)




Это уже многолетний вопрос, но, будучи очень популярным, стоит упомянуть фантастический ресурс для изучения модели памяти C ++ 11. Я не вижу смысла подытоживать его разговор, чтобы сделать этот еще один полный ответ, но учитывая, что это тот парень, который на самом деле написал стандарт, я думаю, что стоит посмотреть разговор.

Herb Sutter имеет трехчасовой разговор о модели памяти C ++ 11 под названием «атомное оружие», доступное на сайте Channel9 - часть 1 и часть 2 . Разговор довольно технический и охватывает следующие темы:

  1. Оптимизации, расы и модель памяти
  2. Заказ - Что: Приобретать и выпускать
  3. Заказ - Как: Мьютекс, Атомная техника и / или Заборы
  4. Другие ограничения на компиляторы и аппаратные средства
  5. Код Gen & Performance: x86 / x64, IA64, POWER, ARM
  6. Расслабленная атомная энергия

В разговоре не говорится об API, а скорее о рассуждениях, предпосылках под капотом и за кулисами (знаете ли вы, что смягченная семантика была добавлена ​​к стандарту только потому, что POWER и ARM не поддерживают синхронизированную нагрузку эффективно?).




Для языков, не определяющих модель памяти, вы пишете код для языка и модель памяти, указанную в архитектуре процессора. Процессор может выбрать способ переназначения доступа к памяти для производительности. Итак, если ваша программа имеет расы данных (гонка данных - это когда несколько ядер / гиперпотоков могут одновременно обращаться к одной и той же памяти), то ваша программа не является кросс-платформой из-за ее зависимости от модели памяти процессора. Вы можете обратиться к руководствам по программному обеспечению Intel или AMD, чтобы узнать, как процессоры могут перенаправлять обращения к памяти.

Очень важно, что блокировки (и семантика параллелизма с блокировкой) обычно реализуются кросс-платформенным способом ... Итак, если вы используете стандартные блокировки в многопоточной программе без расчётов данных, вам не нужно беспокоиться о модах памяти с межплатформенной памятью ,

Интересно, что компиляторы Microsoft для C ++ имеют семантику получения / выпуска для volatile, которая является расширением C ++, чтобы справиться с отсутствием модели памяти на C ++. http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx . Однако, учитывая, что Windows работает только на x86 / x64, это мало говорит (модели памяти Intel и AMD позволяют легко и эффективно реализовать семантику получения / выпуска на языке).




Related