it-swarm.com.ru

Многопоточность без блокировки - для настоящих экспертов

Я читал ответ , что Джон Скит дал вопрос, и в нем он упомянул следующее:

Насколько я понимаю, многопоточность без блокировок предназначена для настоящих экспертов по многопоточности, из которых я не один.

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

Таким образом, мой вопрос - помимо изучения всего, что вы можете о многопоточности и т.д., С чего вы начинаете пытаться научиться специально писать многопотоковый код без блокировок и какие есть хорошие ресурсы.

Ура

84
vdhant

Текущие реализации без блокировок в большинстве случаев следуют одной и той же схеме:

  • * прочитайте некоторое состояние и сделайте копию этого **
  • * Правка копию **
  • выполнить блокированную операцию
  • повторите попытку, если это не удалось

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

Последний бит очень похож на спинлок. На самом деле это основной spinlock . :)
Я согласен с @nobugz в этом: стоимость операций с блокировкой, используемых в многопоточности без блокировки, преобладают задачи, связанные с кэшем и согласованием памяти, которые она должна выполнять .

Однако, что вы получаете с "свободной от блокировок" структурой данных, так это то, что ваши "блокировки" очень мелкозернистые . Это уменьшает вероятность того, что два параллельных потока получают доступ к одной и той же "блокировке" (ячейке памяти).

Хитрость в большинстве случаев заключается в том, что у вас нет выделенных блокировок - вместо этого вы лечите, например, все элементы в массиве или все узлы в связанном списке как "спин-блокировка". Вы читаете, изменяете и пытаетесь обновить, если с момента вашего последнего чтения обновления не было. Если было, вы повторите попытку.
Это делает вашу "блокировку" (о, извините, не блокирующуюся :) очень мелкозернистой, без введения дополнительных требований к памяти или ресурсам.
Делая это более мелкозернистым, уменьшает вероятность ожидания. Звучание как можно более мелким, без дополнительных требований к ресурсам звучит замечательно, не так ли?

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

Теперь, против интуиции, мы уверены, что последовательность кода не передается "сверху вниз", вместо этого она работает так, как будто последовательности вообще не было, и ее можно назвать "игровой площадкой дьявола". Я полагаю, что невозможно дать точный ответ относительно того, какая перестановка заказов на загрузку/хранение будет иметь место. Вместо этого всегда говорят в терминах mays и mights и банок и готовимся к худшему. "О, процессор может переупорядочить эту операцию чтения до этой записи, поэтому лучше поставить барьер памяти прямо здесь, на этом месте".

Вопросы осложняются тем фактом, что даже эти майские и мощи могут отличаться через архитектуру процессора. Например, может случиться так, что гарантированно не произойдет в одной архитектуре может произойти в другой.


Чтобы получить многопоточность без блокировок, вы должны понимать модели памяти.
Получение правильной модели памяти и гарантий не является тривиальным, как демонстрирует эта история, в которой Intel и AMD внесли некоторые исправления в документацию MFENCE, вызвав некоторую путаницу среди разработчиков JVM , Как оказалось, документация, на которую опирались разработчики с самого начала, была не такой точной.

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

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

Есть также "старый, но голди" от Вэнса Моррисона: Что каждый разработчик должен знать о многопоточных приложениях .

... и, конечно же, как @ Eric упомянуто, Joe Duffy является окончательным прочтением на эту тему.

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

Если вы не фанат только для .NET, Даг Ли проделал большую работу в JSR-166 .
Клифф Клик интересный подход к хеш-таблицам, который не использует чередование блокировок - как это делают параллельные хеш-таблицы Java и ​​.NET - и, похоже, хорошо масштабируется до 750 процессоров.

Если вы не боитесь выходить на территорию Linux, в следующей статье вы узнаете о внутренних особенностях современных архитектур памяти и о том, как совместное использование строк кэша может снизить производительность: Что должен знать каждый программист о памяти .

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

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

Я предполагаю, что вопрос OP не о достоинствах и субъективных аргументах за/против какого-либо решения, но если бы мне пришлось ответить на это, я думаю, это зависит от задачи: для построения низкоуровневых, высокопроизводительных базовых структур данных, которые работают на одна система с многими ядрами , либо с низким уровнем блокировки/"lock- "свободные" методы или STM дадут наилучшие результаты с точки зрения производительности и, вероятно, превзойдут MPI решение в любое время с точки зрения производительности, даже если вышеуказанные складки будут сглажены, например, в Эрланге.
Для создания чего-то более сложного, работающего в одной системе, я бы, возможно, выбрал классическую грубую блокировку или, если производительность вызывает большое беспокойство, STM.
Для построения распределенной системы система MPI, вероятно, сделала бы естественный выбор.
Обратите внимание, что есть реализации MPI для также. NET (хотя они, кажется, не так активны).

95
Andras Vass

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

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

Он также пишет блог на эти темы.

Хитрость в правильном создании программ с низким уровнем блокировки состоит в том, чтобы на глубоком уровне точно понять , каковы правила модели памяти для вашей конкретной комбинации оборудования, операционная система и среда выполнения.

Лично я не настолько умён, чтобы делать правильное программирование с минимальными блокировками, кроме InterlockedIncrement, но, если вы, здорово, сделайте это. Просто убедитесь, что вы оставили много кода в коде, чтобы люди, которые не такие умные, как вы, случайно не сломали один из ваших инвариантов модели памяти и не обнаружили ошибку, которую невозможно найти.

27
Eric Lippert

В наши дни не существует такого понятия, как "поточная обработка без блокировки". Это была интересная площадка для научных кругов и тому подобное, еще в конце прошлого века, когда компьютерное оборудование было медленным и дорогим. алгоритм Деккера всегда был моим любимым, современное оборудование выставляло его на пастбище. Это больше не работает.

На этом закончились два события: растущее несоответствие между скоростью RAM и ​​процессором. И способность производителей чипов поставить более одного ядра процессора на чип.

Проблема скорости RAM требовала, чтобы разработчики микросхем поместили буфер в микросхему процессора. В буфере хранятся код и данные, быстро доступные для ядра процессора. И может быть прочитан и записан из/в RAM с гораздо меньшей скоростью. Этот буфер называется кэшем ЦП, большинство ЦП имеют как минимум два из них. Кэш 1-го уровня маленький и быстрый, 2-й - большой и медленный. Пока процессор может читать данные и инструкции из кэша 1-го уровня, он будет работать быстро. Промах кеша действительно дорогой, он переводит процессор в спящий режим на 10 циклов, если данные не находятся в 1-м кеше, и на 200 циклов, если он не находится во 2-м кеше, и его необходимо прочитать из БАРАН.

Каждое ядро ​​ЦП имеет свой кэш, они хранят свой собственный "вид" ОЗУ. Когда процессор записывает данные, запись производится в кэш, который затем медленно сбрасывается в RAM. Неизбежно, каждое ядро ​​теперь будет иметь другое представление о содержимом RAM. Другими словами, один ЦП не знает, что записал другой ЦП, пока этот цикл записи RAM не завершится и ЦП не обновит свой собственный взгляд.

Это совершенно несовместимо с потоками. Вы всегда действительно заботитесь о состоянии другого потока, когда вы должны прочитать данные, которые были записаны другим потоком. Для этого вам нужно явно запрограммировать так называемый барьер памяти. Это низкоуровневый примитив ЦП, который гарантирует, что все кэши ЦП находятся в согласованном состоянии и имеют актуальное представление об ОЗУ. Все ожидающие записи должны быть сброшены в ОЗУ, затем необходимо обновить кэши.

Это доступно в .NET, метод Thread.MemoryBarrier () реализует его. Учитывая, что это 90% работы, которую выполняет оператор блокировки (и 95 +% времени выполнения), вы просто не впереди, избегая инструментов, которые предоставляет вам .NET, и пытаясь реализовать свои собственные.

19
Hans Passant

Google для блокировка свободных структур данных и программная транзакционная память .

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

6
Marcelo Cantos

Когда дело доходит до многопоточности, вы должны точно знать, что вы делаете. Я имею в виду изучить все возможные сценарии/случаи, которые могут возникнуть при работе в многопоточной среде. Многопоточность без блокировок - это не библиотека или класс, который мы включаем, это знания/опыт, которые мы зарабатываем во время нашего путешествия по потокам.

0
bragboy

Несмотря на то, что многопоточность без блокировок может быть затруднена в .NET, часто вы можете добиться значительных улучшений при использовании блокировки, изучая именно то, что нужно заблокировать, и минимизируя заблокированный раздел ... это также называется минимизацией блокировки зернистость .

Например, просто скажите, что вам нужно сделать поток коллекции безопасным. Не просто слепо бросайте блокировку вокруг метода, перебирающего коллекцию, если он выполняет какую-то ресурсоемкую задачу для каждого элемента. Возможно, вам нужно лишь заблокировать создание мелкой копии коллекции. Итерации по копии могут работать без блокировки. Конечно, это сильно зависит от специфики вашего кода, но я смог исправить проблему lock convoy с этим подходом.

0
dodgy_coder