Беззамінна багатопоточність призначена для справжніх експертів з різьблення


86

Я читав відповідь, яку Джон Скіт дав на питання, і в ній він згадав це:

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

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

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

Ура


Я використовую платформи gcc, linux та X86 / X68. Без замків це майже не так складно, як усі вони дають це звучати! Атомні вбудовані ГКК мають бар'єри пам'яті для Intel, але це не має значення в реальному житті. Важливо те, що пам’ять модифікується атомно. Це просто затрясає, коли ви проектуєте структури, що не містять блокування, що не має значення, коли інший потік побачить зміни. Одні зв’язані списки, пропустити списки, хеш-таблиці, безкоштовні списки тощо - це все досить просто зробити безкоштовно. Блокування безкоштовно не для всього. Це просто ще один інструмент, який підходить для певних ситуацій.
johnnycrash


Голосування за закриття як рекомендація ресурсу або незрозуміле, про що ви просите.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Відповіді:


100

Поточні реалізації "без блокування" більшу частину часу дотримуються однакової схеми:

  • * прочитати деякий стан і зробити його копію **
  • * змінити копію **
  • виконайте блоковану операцію
  • повторіть спробу, якщо це не вдається

(* необов’язково: залежить від структури даних / алгоритму)

Останній біт моторошно схожий на спінклок. Насправді це основний спінлок . :)
Я погоджуюсь з @nobugz у цьому: у вартості блокованих операцій, що використовуються в багатоблоковій багатопотоковій роботі, домінують завдання кеш-пам’яті та зв’язності пам’яті, які вона повинна виконувати .

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

Фокус у більшості випадків полягає у тому, що у вас немає виділених блокувань - замість цього ви трактуєте, наприклад, усі елементи масиву або всі вузли у зв’язаному списку як «відкручування». Ви читаєте, модифікуєте та намагаєтесь оновити, якщо оновлення не було з моменту останнього прочитання. Якщо така була, повторіть спробу.
Це робить ваше "блокування" (о, вибачте, не блокується :) дуже дрібним, без введення додаткових вимог до пам'яті чи ресурсів.
Якщо зробити його більш дрібнозернистим, зменшується ймовірність очікування. Зробити його якомога дрібнішим без введення додаткових вимог до ресурсів звучить чудово, чи не так?

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

Тепер ми впевнені, що проти інтуїції послідовність коду не протікає "зверху вниз", натомість вона працює так, ніби послідовності взагалі не було - і її можна назвати "ігровим майданчиком диявола". Я вважаю, що неможливо дати точну відповідь щодо того, яке повторне замовлення завантаження / зберігання буде відбуватися. Замість цього один завжди говорить з точки зору Mays і mights і банок і готуватися до гіршого. "О, центральний процесор може переупорядкувати це читання перед тим, як писати, тому найкраще поставити бар'єр пам'яті прямо тут, на цьому місці".

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


Щоб отримати багатопоточність "без блокування", вам слід зрозуміти моделі пам'яті.
Отримати правильну модель пам'яті та гарантії не є тривіальним, як це демонструє ця історія, в якій Intel і AMD внесли деякі виправлення в документацію, що MFENCEспричинило певний ажіотаж серед розробників JVM . Як виявилося, документація, на яку розробники покладались з самого початку, спочатку була не такою точною.

Блокування в .NET призводять до неявного бар’єру пам’яті, тому ви безпечно їх використовуєте (більшу частину часу, тобто ... див., Наприклад, це Джо Даффі - Бред Абрамс - Венс Моррісон, велич щодо лінивої ініціалізації, блокування, мінливості та пам’яті бар’єри. :) (Обов’язково переходьте за посиланнями на цій сторінці.)

Як додатковий бонус ви ознайомитесь із моделлю пам'яті .NET під час побічного квесту . :)

Існує також "oldie but goldie" від Vance Morrison: Що кожен розробник повинен знати про багатопотокові програми .

... і звичайно, як згадував @Eric , Джо Даффі є остаточним прочитанням на цю тему.

Хороший STM може наблизитися до дрібнозернистого блокування, наскільки він отримує, і, ймовірно, забезпечить продуктивність, близьку до рівня, зробленого власноруч. Одним з них є STM.NET з проектів DevLabs MS.

Якщо ви не є фанатом .NET, Дуг Лі зробив велику роботу в JSR-166 .
Cliff Click має цікавий погляд на хеш-таблиці, які не покладаються на розмежування блокування - як це роблять паралельні хеш-таблиці Java та .NET - і, здається, добре масштабуються до 750 процесорів.

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

@Ben зробив багато коментарів щодо MPI: Я щиро погоджуюсь, що MPI може блищати в деяких сферах. Рішення, засноване на MPI, може бути простіше міркувати, легше впроваджувати і менш схильне до помилок, ніж напівпечена реалізація блокування, яка намагається бути розумною. (Однак це - суб'єктивно - також стосується рішення, заснованого на STM.) Я б також поспорився, що легше писати пристойну розподілену програму, наприклад, в Erlang, на легкі роки , як свідчать багато успішні приклади.

Однак MPI має власні витрати та свої проблеми, коли він працює на одній багатоядерній системі . Наприклад, в Erlang є проблеми, які слід вирішити щодо синхронізації планування процесу та черг повідомлень .
Крім того, у своїй основі системи MPI зазвичай реалізують своєрідне спільне планування N: M для "полегшених процесів". Це, наприклад, означає, що існує неминучий контекстний перехід між легкими процесами. Це правда, що це не "класичний перемикач контексту", а в основному операція простору користувача, і це можна зробити швидко - однак я щиро сумніваюся, що це може бути здійснено за 20-200 циклів, які вимагає блокована операція . Перемикання контексту в режимі користувача, безумовно, відбувається повільнішенавіть у бібліотеці Intel McRT. N: Планування M з легкими процесами не є новим. LWP були там у Солярісі довгий час. Вони були покинуті. У НТ були волокна. Зараз вони в основному є реліквією. У NetBSD були "активації". Вони були покинуті. Linux мав власний підхід до теми потоків N: M. На сьогодні це, здається, дещо мертве.
Час від часу з’являються нові претенденти: наприклад, McRT від Intel , або зовсім недавно планування користувацького режиму разом із ConCRT від Microsoft.
На найнижчому рівні вони роблять те, що робить планувальник MPI N: M. Erlang - або будь-яка система MPI - може отримати велику вигоду для систем SMP, використовуючи нову систему UMS .

Я думаю, питання OP не стосується достоїнств та суб'єктивних аргументів за / проти будь-якого рішення, але якщо мені довелося відповісти на це, я думаю, це залежить від завдання: для побудови низькорівневих, високопродуктивних базових структур даних, які працюють на одинарна система з великою кількістю ядер , або техніка з низьким блокуванням / "без блокування", або STM дадуть найкращі результати з точки зору продуктивності і, ймовірно, в будь-який час перевершать рішення MPI, навіть якщо вищезазначені зморшки виправити наприклад, в Ерлангу.
Для створення чогось помірно складнішого, що працює на одній системі, я б, можливо, обрав класичний грубозернистий замок або, якщо продуктивність викликає велике занепокоєння, STM.
Для побудови розподіленої системи система MPI, мабуть, зробила б природний вибір.
Зверніть увагу, що існують реалізації MPI і для .NET (хоча вони, здається, не такі активні).


1
Незважаючи на те, що ця відповідь містить багато корисної інформації, ідея заголовка про те, що алгоритми та структури даних, що не містять замків, є по суті лише сукупністю дуже дрібних зернистих блоків, є помилковою. Незважаючи на те, що ви зазвичай бачите цикли повторної спроби в структурах, що не містять блокування, поведінка дуже інша: блокування (включаючи спінлоки) отримують виключно деякий ресурс, а інші потоки не можуть прогресувати, поки він утримується. "Повторна спроба" в цьому сенсі просто чекає випуску ексклюзивного ресурсу.
BeeOnRope

1
З іншого боку, безблокувальні алгоритми не використовують CAS або інші атомні інструкції для отримання ексклюзивного ресурсу, а для завершення певної операції. Якщо вони не вдаються, це через тимчасово дрібнозернисту гонку з іншою ниткою, і в такому випадку інша нитка досягла прогресу (завершила свою роботу). Якщо потік невизначений підозріло, усі інші потоки все ще можуть прогресувати. Це як якісно, ​​так і за ефективністю дуже відрізняється від ексклюзивних замків. Кількість "повторних спроб", як правило, дуже низька для більшості циклів CAS, навіть в умовах
суворих

1
... але це, звичайно, не означає хорошого масштабування: суперечка за одне розташування пам’яті на машинах SMP завжди буде досить повільною, лише через міжядерні затримки між сокетами, навіть якщо кількість збоїв CAS дорівнює низький.
BeeOnRope

1
@AndrasVass - Я думаю, це також залежить від "хорошого" проти "поганого" коду без блокування. Звичайно, будь-хто може написати структуру і назвати її безконтактною, хоча вона насправді просто використовує блокування користувацького режиму і навіть не відповідає визначенню. Я б також закликав усіх зацікавлених читачів ознайомитись з цією статтею від Herlihy та Shavit, яка формально розглядає різні категорії алгоритмів, що базуються на замках та без них. Будь-що Герлігове на цю тему також рекомендується прочитати.
BeeOnRope

1
@AndrasVass - я не згоден. Більшість класичних структур, що не містять блокування (списки, черги, одночасні карти тощо), не мали обертання навіть для спільних змінних структур, а практичні існуючі реалізації однакових, наприклад, Java, дотримуються тієї ж моделі (я не такий, як знайомі з тим, що доступне в компільованій власною мовою C або C ++, і там складніше через відсутність збору сміття). Можливо, ми з вами по-різному визначаємо прядіння: я не вважаю "CAS-повторне спробу", яке ви знайдете у вільних від замків матеріалах, "спінінг". "Обертання" IMO передбачає гаряче очікування.
BeeOnRope

27

Книга Джо Даффі:

http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html

Він також пише блог на ці теми.

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

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


38
Тож якщо і Ерік Ліпперт, і Джон Скіт вважають, що безкоштовне програмування призначене лише для людей розумніших за них самих, то я негайно втечу від крику від цієї ідеї. ;-)
dodgy_coder

20

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

На цьому закінчилися дві події: зростання диспропорції між швидкістю оперативної пам’яті та центральним процесором. І здатність виробників чіпів розміщувати на чіпі більше одного ядра процесора.

Проблема зі швидкістю оперативної пам'яті вимагала від дизайнерів мікросхем вставити буфер на чіп процесора. Буфер зберігає код і дані, швидко доступні ядром центрального процесора. І їх можна читати і записувати з / в оперативну пам’ять набагато повільніше. Цей буфер називається кешем процесора, більшість процесорів мають принаймні два з них. Кеш-пам’ять 1-го рівня невеликий і швидкий, 2-й великий і повільніший. Поки центральний процесор може читати дані та інструкції з кешу 1-го рівня, він працюватиме швидко. Пропуск кешу дійсно дорогий, він призводить до сну процесора протягом 10 циклів, якщо дані відсутні в 1-му кеші, до 200 циклів, якщо їх немає у 2-му кеші, і їх потрібно читати з ОЗП.

Кожне ядро ​​центрального процесора має свій власний кеш, вони зберігають свій власний "вигляд" оперативної пам'яті. Коли процесор записує дані, запис робиться в кеш, який потім повільно переноситься в оперативну пам'ять. Неминуче, кожне ядро ​​тепер матиме різний погляд на вміст оперативної пам'яті. Іншими словами, один процесор не знає, що написав інший процесор, поки цей цикл запису в оперативну пам'ять не завершиться, і процесор не оновить власний вигляд.

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

Це доступно в .NET, метод Thread.MemoryBarrier () реалізує такий. Враховуючи, що це 90% роботи, яку виконує оператор блокування (і 95 +% часу виконання), ви просто не випереджаєте, уникаючи інструментів, які надає вам .NET, і намагаєтеся реалізувати власні.


2
@ Davy8: склад робить це все ще важким. Якщо у мене є два хеш-таблиці без замків, і як споживач я отримую доступ до них обох, це не буде гарантувати узгодженість стану в цілому. Найближчі сьогодні ви можете отримати STM, де ви можете розмістити два доступу, наприклад, в одному atomicблоці. Загалом, споживання беззамкових конструкцій у багатьох випадках може бути настільки ж складним.
Andras Vass,

4
Можливо, я помиляюся, але я думаю, ви неправильно пояснили, як працює когерентність кеш-пам'яті. Більшість сучасних багатоядерних процесорів мають когерентні кеші, а це означає, що апаратне забезпечення кеш-пам'яті обробляє, переконуючись, що всі процеси мають однаковий вигляд вмісту ОЗУ - блокуючи виклики "читання", поки не завершаться всі відповідні виклики "запису". Документація Thread.MemoryBarrier () ( msdn.microsoft.com/en-us/library/… ) взагалі нічого не говорить про поведінку кешу - це просто директива, яка заважає процесору переупорядковувати читання та запис.
Брукс Мойсей

7
"На сьогоднішній день не існує такого поняття, як" різьблення без замків ". Скажіть це програмістам Erlang та Haskell.
Джульєтта

4
@HansPassant: "На сьогоднішній день не існує такого поняття, як" блокування потоків "". F #, Erlang, Haskell, Cilk, OCaml, Паралельна бібліотека завдань Microsoft (TPL) та Потокові будівельні блоки Intel (TBB) заохочують багатопотокове програмування без блокування. У наші дні я рідко використовую замки у виробничому коді.
JD

5
@HansPassant: "так званий бар'єр пам'яті. Це примітив центрального процесора низького рівня, який гарантує, що всі кеші процесора перебувають у стабільному стані та мають сучасний вигляд оперативної пам'яті. Усі очікувані записи повинні бути очищені в RAM, кеші потім потрібно оновити ". Бар'єр пам'яті в цьому контексті запобігає переупорядкуванню інструкцій пам'яті (завантаження та зберігання) компілятором або процесором. Нічого спільного з послідовністю кеш-пам'яті процесора.
JD

6

Google заблокував безкоштовні структури даних та програмну транзакційну пам’ять .

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


0

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


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

Я бачу обидві сторони цього. Щоб отримати ефективну продуктивність із бібліотеки без блокування, потрібно глибоко знати моделі пам'яті. Але програміст, який не має цих знань, все одно може скористатися перевагами правильності.
Бен Войгт,

0

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.