C ++ 11 представив стандартизовану модель пам'яті. Що це означає? І як це вплине на програмування на C ++?


1894

C ++ 11 представив стандартизовану модель пам'яті, але що це саме означає? І як це вплине на програмування на C ++?

У цій статті (від Гевіна Кларка, який цитує Герба Саттера ) сказано, що:

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

"Коли ви говорите про розділення [коду] на різні ядра, що є у стандарті, ми говоримо про модель пам'яті. Ми будемо оптимізувати її, не порушуючи наступних припущень, які люди збираються робити в коді", - сказав Саттер .

Ну, я можу запам'ятати цей та подібні пункти, доступні в Інтернеті (так як у мене була своя модель пам’яті з моменту народження: P) і навіть можу публікувати як відповідь на запитання інших, але якщо чесно, я точно не розумію це.

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

Я також відчуваю, що модель пам'яті C ++ 11 якимось чином пов'язана з підтримкою багатопотокової C ++ 11, як я часто бачу цих двох разом. Якщо так, то як саме? Чому вони повинні бути пов’язані?

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


3
@curiousguy: Розробляти ...
Наваз

4
@curiousguy: Напишіть щоденник тоді ... і запропонуйте виправити. Немає іншого способу зробити свою думку справедливою та обґрунтованою.
Наваз

2
Я сприйняв цей сайт як місце, щоб запитати Q та обмінятися ідеями. Моє ліжко; це місце для відповідності, де ви не можете не погодитися з Herb Sutter, навіть коли він відверто суперечить собі щодо специфікації кидка.
curiousguy

5
@curiousguy: C ++ - це те, що говорить Стандарт, а не те, що говорить випадковий хлопець в Інтернеті. Так, так, повинно бути відповідність Стандарту. C ++ НЕ є відкритою філософією, де можна говорити про все, що не відповідає Стандарту.
Наваз

3
"Я довів, що жодна програма C ++ не може мати чітко визначену поведінку". . Високі претензії, без жодних доказів!
Наваз

Відповіді:


2204

По-перше, ви повинні навчитися мислити як юрист з мов.

Специфікація 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 їх стандартизує. Але іноді з міркувань продуктивності ви хочете примітивів нижчого рівня (наприклад, класичну схему блокування з подвійною перевіркою ). Новий стандарт надає гаджети високого рівня, такі як мутекси та змінні стану, а також надає гаджети низького рівня, такі як атомні типи та різні аромати бар'єру пам'яті. Отже, тепер ви можете писати складні, високопродуктивні паралельні процедури повністю в межах мови, визначеної стандартом, і ви можете бути впевнені, що ваш код буде компілюватися та працювати без змін як на сьогоднішніх системах, так і на завтрашніх.

Хоча, якщо чесно, якщо ви не експерт і працюєте над серйозним кодом низького рівня, ви, ймовірно, повинні дотримуватися мітекси та змінних умов. Це те, що я маю намір зробити.

Детальніше про цей матеріал читайте у цій публікації в блозі .


37
Приємна відповідь, але це справді прохання про деякі фактичні приклади нових примітивів. Крім того, я думаю, що впорядкованість пам'яті без примітивів така ж, як і до -C ++ 0x: гарантій немає.
Джон Ріплі

5
@John: Я знаю, але я все ще вчу сам примітивів :-). Крім того, я думаю, що вони гарантують доступ до байтів атомним (хоча і не впорядкованим), тому я пішов з "char" для свого прикладу ... Але я навіть не впевнений у цьому на 100% ... Якщо ви хочете запропонувати будь-які добрі " підручник "посилання Я додам їх до своєї відповіді
Немо,

48
@Nawaz: Так! Доступ до пам'яті може бути упорядкований компілятором або процесором. Подумайте про (наприклад) сховищах та спекулятивних навантаженнях. Порядок, в який потрапляє системна пам'ять, може бути не таким, як кодований вами. Компілятор і процесор забезпечать, щоб такі переупорядкування не порушували однопотоковий код. Для багатопотокового коду "модель пам'яті" характеризує можливі переупорядкування, і що станеться, якщо дві нитки читають / записують одне і те ж місце розташування одночасно, і як ви здійснюєте контроль над обома. Для однопотокового коду модель пам'яті не має значення.
Немо

26
@Nawaz, @Nemo - Незначна деталь: нова модель пам’яті є актуальною для однопотокового коду, оскільки вона визначає невизначеність певних виразів, таких як i = i++. Стара концепція точок послідовності була відкинута; новий стандарт визначає те саме, використовуючи відношення послідовності перед тим, що є лише особливим випадком більш загальної міжпотокової концепції, що відбувається раніше .
ЙоханнесD

17
@ AJG85: У розділі 3.6.2 проекту C ++ 0x специфікації сказано: "Змінні зі статичною тривалістю зберігання (3.7.1) або тривалістю зберігання потоку (3.7.2) повинні бути ініціалізовані нулем (8.5) до того, як відбудеться будь-яка інша ініціалізація місце ». Оскільки x, y є глобальними в цьому прикладі, вони мають статичну тривалість зберігання і, отже, я вважаю нульовою ініціалізацією.
Немо

345

Я просто наведу аналогію, з якою я розумію моделі послідовності пам’яті (або моделі пам'яті, коротше). Він натхненний семінарним документом Леслі Лампорта "Час, годинник та впорядкування подій у розподіленій системі" . Аналогія є влучною і має фундаментальне значення, але може бути надмірною для багатьох людей. Однак, я сподіваюся, що він створює ментальний образ (живописне зображення), що полегшує міркування про моделі послідовності пам'яті.

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

Цитуючи "Буквар про послідовність пам'яті та узгодженість кешу"

Інтуїтивна (і найбільш обмежена) модель пам'яті - це послідовна послідовність (SC), в якій багатопотокове виконання повинно виглядати як переплетення послідовних виконань кожного складового потоку, як ніби потоки мультиплексовані за часом на одноядерному процесорі.

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

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

[Зображення з Вікіпедії] Зображення з Вікіпедії

Читачі, знайомі зі спеціальною теорією відносності Ейнштейна , помітять, на що я натякаю. Переклад слів Мінковського у сферу моделей пам'яті: адресний простір та час - це тіні адрес-простір-час. У цьому випадку кожен спостерігач (тобто потік) проектуватиме тіні подій (тобто запам'ятовування / навантаження пам'яті) на свою власну світову лінію (тобто свою часову вісь) та свою власну площину одночасності (його вісь адресно-простір) . Нитки в моделі пам'яті C ++ 11 відповідають спостерігачам , які рухаються відносно один одного в особливій відносності. Послідовна послідовність відповідає галілейському простору-часу (тобто всі спостерігачі погоджуються на один абсолютний порядок подій та глобальне відчуття одночасності).

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

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

У моделі пам'яті C ++ 11 аналогічний механізм (модель консистенції придбання-випуску) використовується для встановлення цих локальних зв'язків причинності .

Щоб дати визначення послідовності пам’яті та мотивацію відмови від SC, я цитую «Підручник з консистенції пам’яті та узгодженості кешу».

Для спільної машини пам'яті модель послідовності пам’яті визначає архітектурно видиму поведінку її системи пам’яті. Критерій коректності поведінки розділів ядра одного процесора між " одним правильним результатом " та " безліччю невірних альтернатив ". Це пояснюється тим, що архітектура процесора зобов’язує виконання потоку перетворювати заданий вхідний стан в єдиний чітко визначений вихідний стан, навіть на ядрі, що знаходиться поза порядком. Моделі сумісності спільної пам’яті, однак, стосуються навантажень і запасів декількох потоків і, як правило, дозволяють зробити багато правильних виконаньзабороняючи при цьому багато (більше) неправильних. Можливість декількох правильних виконань обумовлена ​​ISA, що дозволяє виконувати кілька потоків одночасно, часто з багатьма можливими юридичними переплетеннями інструкцій з різних потоків.

Розслаблені або слабкі моделі консистенції пам’яті мотивовані тим, що більшість впорядкованих пам’яті у сильних моделях непотрібні. Якщо потік оновлює десять елементів даних, а потім прапор синхронізації, програмістам, як правило, байдуже, чи оновлюються елементи даних один щодо одного, але лише те, що всі елементи даних оновлюються перед оновленням прапора (зазвичай реалізується за допомогою інструкцій FENCE ). Розслаблені моделі прагнуть зафіксувати цю підвищену гнучкість замовлення та зберегти лише ті замовлення, яких вимагають програмісти”, Щоб отримати як більш високу продуктивність, так і коректність SC. Наприклад, у деяких архітектурах буфери запису FIFO використовуються кожним ядром для зберігання результатів закріплених (звільнених) сховищ перед записом результатів у кеші. Ця оптимізація підвищує продуктивність, але порушує SC. Буфер запису приховує затримку обслуговування пропуску магазину. Оскільки магазини поширені, важлива перевага - уникнути зупинки на більшості з них. Для одноядерного процесора буфер запису може бути зроблений архітектурно невидимим, гарантуючи, що навантаження для адреси A повертає значення останнього сховища в A, навіть якщо один або кілька сховищ до A знаходяться в буфері запису. Зазвичай це робиться шляхом обходу значення останнього магазину на A до навантаження від А, де "найновіший" визначається програмним замовленням, або відклавши навантаження A, якщо в буфері запису зберігається сховище до A. Коли використовується декілька ядер, у кожного буде власний обхідний буфер запису. Без буферів запису апаратне забезпечення є SC, але з буферами запису це не відбувається, роблячи буфери запису архітектурно видимими в багатоядерному процесорі.

Переупорядкування магазину-магазину може статися, якщо ядро ​​має не-FIFO-буфер запису, який дозволяє магазинам відходити в іншому порядку, ніж у тому порядку, в якому вони введені. Це може статися, якщо перший сховище пропущено в кеш-пам'яті, тоді як другий звернеться або якщо другий сховище може поєднатися з попереднім сховищем (тобто перед першим сховищем). Переналагодження навантажувального навантаження також може відбуватися на динамічно запланованих ядрах, які виконують інструкції поза програмою. Це може поводитись так само, як упорядкування магазинів на іншому ядрі (Ви можете придумати приклад переплетення між двома потоками?). Переупорядкування більш раннього завантаження з пізнішим сховищем (перенастроювання сховища для завантаження) може спричинити багато неправильних способів поведінки, наприклад завантаження значення після звільнення блокування, яке захищає його (якщо магазин - це операція розблокування).

Оскільки когерентність кешу та послідовність пам’яті іноді плутають, доцільно також мати цю цитату:

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

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


52
+1 для аналогії зі спеціальною відносністю, я намагався зробити ту саму аналогію. Занадто часто я бачу програмістів, які досліджують потоковий код, намагаючись інтерпретувати поведінку як операції в різних потоках, що відбуваються переплітаються між собою в певному порядку, і я маю їм сказати, ні, для багатопроцесорних систем поняття одночасності між різними <s > рамки посилань </s> нині втрачають сенс. Порівняння зі спеціальною відносністю - хороший спосіб змусити їх поважати складність проблеми.
П'єр Лебеопін

71
Тож варто зробити висновок, що Всесвіт багатоядерний?
Петро К

6
@PeterK: Саме так :) І ось дуже приємна візуалізація цієї картини часу фізиком Брайаном Гріном: youtube.com/watch?v=4BjGWLJNPcA&t=22m12s Це "Ілюзія часу [повний документальний фільм]" у хвилині 22 та 12 секунд.
Ахмед Нассар

2
Це тільки я або він переходить з 1D моделі пам'яті (горизонтальна вісь) на 2D модель пам'яті (площини одночасності). Я вважаю це трохи заплутаним, але, можливо, це тому, що я не є носієм мови ... Все ще дуже цікаве прочитання.
Прощай SE

Ви забули важливу частину: " аналізуючи результати завантажень і магазинів " ... без використання точної інформації про терміни.
curiousguy

115

Це вже багаторічне запитання, але, маючи велику популярність, варто згадати фантастичний ресурс для ознайомлення з моделлю пам'яті 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 не підтримують синхронізоване завантаження ефективно?).


10
Ця розмова справді фантастична, загалом варто того 3 годин, які ви витратите на її перегляд.
ZunTzu

5
@ZunTzu: для більшості відеоплеєрів ви можете встановити швидкість у 1,25, 1,5 або навіть у 2 рази більше від оригіналу.
Крістіан Северин

4
@eran У вас, хлопці, трапляються слайди? посилання на каналі 9 сторінок розмови не працюють.
афон

2
@athos У мене їх немає, вибачте. Спробуйте зв’язатися з каналом 9, я не думаю, що видалення було навмисним (я здогадуюсь, що вони отримали посилання від Herb Sutter, розміщене як є, і він пізніше видалив файли; але це лише припущення ...).
еран

75

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

Якщо ви говорите про потоки POSIX або потоки Windows, то це трохи ілюзія, оскільки ви насправді говорите про потоки x86, оскільки це апаратна функція, яка працює одночасно. Модель пам'яті C ++ 0x гарантує, чи ви користуєтеся x86, або ARM, або MIPS , або будь-яким іншим, що можете придумати.


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

57

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

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

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


2
Це правда, що, коли відповідь була написана, Windows працює лише на x86 / x64, але Windows в якийсь момент працює на IA64, MIPS, Alpha AXP64, PowerPC та ARM. Сьогодні він працює на різних версіях ARM, що відрізняється від пам'яті x86, і ніде не прощає.
Лоренцо Дематте

Цей зв'язок дещо розірваний (йдеться про "Візуальну студію документації на пенсію 2005" ). Хочете оновити його?
Пітер Мортенсен

3
Це було неправдою навіть тоді, коли відповідь була написана.
Бен

" отримати доступ до тієї самої пам'яті одночасно ", щоб отримати доступ конфліктним шляхом
curiousguy

27

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

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

Раніше атоміка робилася б за допомогою компіляторів, а також бібліотеки вищого рівня. Огорожі були б зроблені за допомогою інструкцій для процесора (бар'єри пам'яті).


19
Проблема раніше полягала в тому, що не було такого поняття, як mutex (з точки зору стандарту C ++). Тож єдиними гарантіями, які ви надали, був виробник mutex, що було добре, доки ви не перенесли код (оскільки незначні зміни до гарантій важко помітити). Тепер ми отримуємо гарантії, передбачені стандартом, який повинен бути переносним між платформами.
Мартін Йорк

4
@Martin: у будь-якому випадку одне - це модель пам’яті, а інша - атоміка та примітивні нитки, що працюють над цією моделлю пам'яті.
ніндзя

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

3
Це також може бути справжньою проблемою для людей, які намагаються написати бібліотеку mutex. Коли процесор, контролер пам'яті, ядро, компілятор і "бібліотека С" реалізуються різними командами, і деякі з них сильно розходяться з приводу того, як цей матеріал повинен працювати, ну, іноді речі ми системні програмісти повинні зробити, щоб представити гарний фасад на рівні програм зовсім не приємно.
zwol

11
На жаль, недостатньо для захисту ваших структур даних простими мутексами, якщо на вашій мові немає стійкої моделі пам'яті. Існують різні оптимізації компілятора, які мають сенс в одному потоковому контексті, але коли вступають у дію декілька потоків та ядер процесора, переупорядкування доступу до пам'яті та інших оптимізацій може спричинити невизначеність поведінки. Для отримання додаткової інформації див. "Нитки не можна реалізувати як бібліотеку" Ганса
Боема

0

Наведені вище відповіді стосуються найбільш фундаментальних аспектів моделі пам'яті C ++. На практиці більшість застосувань std::atomic<>"просто працює", принаймні до тих пір, поки програміст не переоптимізується (наприклад, намагаючись розслабити занадто багато речей).

Є одне місце, де помилки все ще поширені: блокування послідовностей . Про те, що виклики ви знайдете на відмінному та легкому для читання, читайте на https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf . Блоки послідовності є привабливими, оскільки читач уникає писати до замкового слова. Наступний код заснований на рисунку 1 вищезазначеного технічного звіту, і він висвітлює проблеми при впровадженні блокувань послідовностей у C ++:

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

Настільки неінтуїтивні, як це здається спочатку, data1і data2потрібно atomic<>. Якщо вони не є атомними, то їх можна прочитати (в reader()) в той самий час, коли вони написані (в writer()). Відповідно до моделі пам'яті C ++, це гонка, навіть якщо reader()ніколи фактично не використовує дані . Крім того, якщо вони не є атомними, компілятор може кешувати перше зчитування кожного значення в регістрі. Очевидно, ви б цього не хотіли ... ви хочете перечитувати під час кожної ітерації whileциклу reader().

Також їх недостатньо зробити atomic<>та отримати доступ до них memory_order_relaxed. Причина цього полягає в тому, що читає НомерСтарт (в reader()) тільки ACQuire семантика. Простіше кажучи, якщо X і Y - це доступ до пам'яті, X передує Y, X не є придбанням або випуском, а Y - придбання, то компілятор може змінити порядок Y перед X. Якщо Y було другим читанням послідовності, а X було зчитування даних, таке переупорядкування порушило б реалізацію блокування.

У статті дано кілька рішень. Один з кращою продуктивністю сьогодні, ймовірно , той , який використовує atomic_thread_fenceз memory_order_relaxed до того другого читання в seqlock. У статті це малюнок 6. Я тут не відтворюю код, тому що кожен, хто читав це далеко, дійсно повинен прочитати папір. Він більш точний і повний, ніж ця посада.

Останнє питання полягає в тому, що може бути неприродно робити dataзмінні атомними. Якщо ви не можете ввести свій код, то вам потрібно бути дуже обережними, тому що лиття з неатомного до атомного є законним лише для примітивних типів. Слід додати C ++ 20 atomic_ref<>, що полегшить вирішення цієї проблеми.

Підводячи підсумок: навіть якщо ви думаєте, що розумієте модель пам'яті C ++, вам слід бути дуже обережними перед тим, як виконувати власні блокування послідовностей.


-2

C і C ++ використовувались для визначення сліду виконання добре сформованої програми.

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

Це означає, що ці визначення мови взагалі не мають сенсу, як жоден логічний метод поєднання цих двох підходів. Зокрема, руйнування мутексної чи атомної змінної недостатньо визначено.


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

1
@MatiasHaeussler Я думаю, ви неправильно прочитали мою відповідь; Я не заперечую проти визначення певної особливості C ++ тут ​​(у мене також багато таких гострих зауважень, але тут немає). Я тут стверджую, що в C ++ (ні в C) немає чітко визначеної конструкції. Вся семантика МТ - це повний безлад, оскільки у вас більше немає послідовної семантики. (Я вважаю, що Java MT зламана, але менше.) "Простим прикладом" була б майже будь-яка програма MT. Якщо ви не погоджуєтесь, ви можете відповісти на моє запитання про те, як довести правильність програм MT C ++ .
curiousguy

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

1
"Чи повинні запитання про демостративність комп'ютерних програм розміщуватися в stackoverflow або в stackexchange (якщо ні в жодному, де)?" Цей, здається, мета для stackoverflow, чи не так?
Matias Haeussler

1
@MatiasHaeussler 1) C і C ++ по суті поділяють "модель пам'яті" атомних змінних, мутексів і багатопотокових. 2) Актуальність цього стосується переваг наявності "моделі пам'яті". Я вважаю, що вигода дорівнює нулю, оскільки модель не є голосною.
curiousguy
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.