Свободная от блокировок многопоточность является настоящей экспертов по поточной обработке

Я прочитывал ответ, который Jon Skeet дал вопросу, и в нем он упомянул это:

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

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

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

Удачи

85
задан Community 23 May 2017 в 11:54
поделиться

5 ответов

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

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

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

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

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

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

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

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

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


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

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

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

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

... и, конечно же, как упомянул @Eric , Джо Даффи является исчерпывающим читателем по этой теме.

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

Если вы не a.Зилот, работающий только с 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 существовали в Solaris очень давно. Они были заброшены. Были волокна в NT. Сейчас они в основном реликвии. В NetBSD были «активации». Они были заброшены. В Linux был свой собственный взгляд на потоки N: M. Кажется, сейчас он несколько мертв.
Время от времени появляются новые претенденты: например, McRT от Intel или совсем недавно User-Mode Scheduling вместе с ConCRT от Microsoft.
На самом низком уровне они делают то же, что и планировщик N: M MPI. Erlang - или любая система MPI - может значительно выиграть в системах SMP, используя новую UMS .

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

98
ответ дан 24 November 2019 в 08:19
поделиться

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

Этому положили конец два события: растущее несоответствие между скоростью ОЗУ и ЦП. И способность производителей микросхем размещать на микросхеме более одного ядра ЦП.

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

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

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

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

19
ответ дан 24 November 2019 в 08:19
поделиться

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

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

6
ответ дан 24 November 2019 в 08:19
поделиться

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

0
ответ дан 24 November 2019 в 08:19
поделиться

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

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

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

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

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

27
ответ дан 24 November 2019 в 08:19
поделиться
Другие вопросы по тегам:

Похожие вопросы: