Текущие реализации "без блокировок" большую часть времени следуют одному и тому же шаблону:
- * прочтите какое-то состояние и сделайте копию **
- * изменить копию **
- сделать взаимосвязанную операцию
- повторить попытку, если это не удалось
(* необязательно: зависит от структуры данных / алгоритма)
Последний бит очень похож на спин-блокировку. Фактически, это простая спин-блокировка . :)
Я согласен с @nobugz в этом: стоимость взаимосвязанных операций, используемых в многопоточности без блокировки, во многом определяется задачами кеширования и когерентности памяти, которые он должен выполнять .
Однако при использовании «свободной от блокировок» структуры данных вы получаете то, что ваши «блокировки» очень мелкие . Это снижает вероятность того, что два параллельных потока обращаются к одной и той же «блокировке» (ячейке памяти).
Уловка в большинстве случаев заключается в том, что у вас нет выделенных блокировок - вместо этого вы обрабатываете, например, все элементы в массиве или все узлы в связанном списке как «спин-блокировку». Вы читаете, изменяете и пытаетесь обновить, если с момента последнего чтения обновлений не было. Если было, попробуйте еще раз.
Это делает вашу "блокировку" (извините, неблокирующую :) очень мелкой, без дополнительных требований к памяти или ресурсам.
Делая его более детализированным, вы уменьшаете вероятность ожидания. Сделать его как можно более детальным без дополнительных требований к ресурсам - это здорово, не правда ли?
Однако большая часть удовольствия может быть получена от обеспечения правильного порядка загрузки / хранения .
Вопреки интуиции, процессоры могут свободно переупорядочивать операции чтения / записи в памяти - кстати, они очень умны: вам будет сложно наблюдать за этим из одного потока. Однако вы столкнетесь с проблемами, когда начнете использовать многопоточность на нескольких ядрах. Ваша интуиция сломается: просто потому, что инструкция находится раньше в вашем коде, это не значит, что это действительно произойдет раньше. Процессоры могут обрабатывать инструкции не по порядку: и им особенно нравится делать это с инструкциями с доступом к памяти, чтобы скрыть задержку основной памяти и лучше использовать свой кеш.
Теперь, вопреки интуиции, очевидно, что последовательность кода не течет «сверху вниз», вместо этого она выполняется так, как будто никакой последовательности вообще не было - и ее можно назвать «площадкой дьявола». Я считаю, что невозможно дать точный ответ относительно того, какие переупорядочения загрузки / магазина будут иметь место. Вместо этого один всегда говорит с точки зрения Mays и mights и банок и готовиться к худшему. «О, ЦП может переупорядочить это чтение, чтобы оно происходило перед записью, поэтому лучше всего поставить барьер памяти прямо здесь, на этом месте».
Вопросы , осложняется тем фактом , что даже эти Mays и mights могут отличаться по архитектуре процессора. Это может быть, например, что - то , что гарантированно не произойдет в одной архитектуре может произойти на другой.
Чтобы получить правильную «свободную от блокировок» многопоточность, вы должны понимать модели памяти.
Однако получение правильной модели памяти и гарантий не является тривиальным делом, как показывает эта история, когда Intel и AMD внесли некоторые исправления в документацию, что MFENCE
вызвало некоторую ажиотаж среди разработчиков JVM . Как оказалось, документация, на которую разработчики полагались с самого начала, изначально была не такой уж точной.
Блокировки в .NET приводят к неявному барьеру памяти, поэтому вы можете безопасно их использовать (большую часть времени, то есть ... см., Например, это величие Джо Даффи - Брэда Абрамса - Вэнса Моррисона в ленивой инициализации, блокировках, нестабильности и памяти барьеры. :) (Обязательно переходите по ссылкам на этой странице.)
В качестве дополнительного бонуса вы познакомитесь с моделью памяти .NET во время побочного квеста . :)
Есть также «старый, но золотой» от Вэнса Моррисона: что каждый разработчик должен знать о многопоточных приложениях .
... и, конечно же, как сказал @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 существовали в Solaris очень давно. Они были заброшены. Были волокна в NT. Сейчас они в основном реликвии. В NetBSD были «активации». Они были заброшены. У Linux был свой собственный взгляд на потоки N: M. Кажется, сейчас он несколько мертв.
Время от времени появляются новые претенденты: например, McRT от Intel или совсем недавно User-Mode Scheduling вместе с ConCRT от Microsoft.
На самом низком уровне они делают то же самое, что и планировщик N: M MPI. Erlang - или любая система MPI - может значительно выиграть в системах SMP, используя новую UMS .
Я предполагаю, что вопрос OP не касается достоинств и субъективных аргументов за / против любого решения, но если бы мне пришлось ответить на это, я полагаю, это зависит от задачи: для создания низкоуровневых высокопроизводительных базовых структур данных, которые работают на одиночная система с множеством ядер , методы low-lock / "lock-free" или STM дадут лучшие результаты с точки зрения производительности и, вероятно, превзойдут решение MPI в любое время с точки зрения производительности, даже если вышеупомянутые морщины будут устранены например, в Эрланге.
Для создания чего-либо умеренно более сложного, работающего в одной системе, я, возможно, выбрал бы классическую крупнозернистую блокировку или, если производительность имеет большое значение, STM.
Для построения распределенной системы, вероятно, будет естественным выбором система MPI.
Обратите внимание, что существуют реализации MPI для .NET (хотя они, похоже, не так активны).