Поточні реалізації "без блокування" більшу частину часу дотримуються однакової схеми:
- * прочитати деякий стан і зробити його копію **
- * змінити копію **
- виконайте блоковану операцію
- повторіть спробу, якщо це не вдається
(* необов’язково: залежить від структури даних / алгоритму)
Останній біт моторошно схожий на спінклок. Насправді це основний спінлок . :)
Я погоджуюсь з @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 (хоча вони, здається, не такі активні).