Lock-free multi-threading для реальных продевая нитку специалистов


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

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

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

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

Ура

6 80

6 ответов:

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

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

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

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

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

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

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

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

дело осложняется тем, что даже эти Майс и mights может отличаться в разных архитектурах ЦП. Это может бывает, например, что что-то такое гарантированно не случится в одной архитектуры может случиться на другой.


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

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

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

есть также "Олди, но Голди" от Вэнса Моррисона:Что Каждый Разработчик Должен Знать О Многопоточных Приложениях.

...и конечно, как @Eric сказано, Джо Даффи это окончательное чтение по этому вопросу.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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