Как я могу написать структуру без блокировки? - PullRequest
36 голосов
/ 18 сентября 2008

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

Как я могу написать структуру без блокировки?

Ответы [ 21 ]

44 голосов
/ 18 сентября 2008

Краткий ответ:

Вы не можете.

Длинный ответ:

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

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

Если вы все еще настаиваете на создании собственной структуры без блокировки, обязательно:

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

Больше чтения:

Блокировка свободных и ожидание бесплатных алгоритмов в Википедии

Херб Саттер: Код без блокировки: ложное чувство безопасности

16 голосов
/ 18 сентября 2008

Используйте библиотеку, такую ​​как Intel's Threading Building Blocks , она содержит довольно много безблокировочных структур и алгоритмов. Я действительно не рекомендовал бы пытаться писать код без блокировки самостоятельно, он чрезвычайно подвержен ошибкам и его трудно получить.

12 голосов
/ 18 сентября 2008

Как указывало sblundy , если все объекты являются неизменяемыми и доступны только для чтения, вам не нужно беспокоиться о блокировке, однако это означает, что вам, возможно, придется много копировать объекты. Копирование обычно включает в себя malloc, а malloc использует блокировку для синхронизации распределения памяти между потоками, поэтому неизменяемые объекты могут купить вас меньше, чем вы думаете (сам malloc масштабируется довольно плохо, а malloc медленный ; если вы делаете много malloc в критический раздел производительности, не ожидайте хорошей производительности).

Когда вам нужно только обновить простые переменные (например, 32- или 64-разрядные целочисленные или указатели), просто выполнить над ними операции сложения или вычитания или просто поменять значения двух переменных, большинство платформ предлагают для этого «атомарные операции» (далее GCC предлагает это также). Atomic - это не то же самое, что потокобезопасный . Однако Atomic гарантирует, что, если один поток записывает 64-битное значение, например, в область памяти, а другой поток читает из нее, считывающий получает значение до операции записи или после операции записи, но никогда не * 1009. * нарушено значение в промежутке между операцией записи (например, та, в которой первые 32 бита уже являются новыми, последние 32 бита остаются старыми значениями! Это может произойти, если вы не используете атомарный доступ к такой переменной ).

Однако, если у вас есть структура C с 3 значениями, которую нужно обновить, даже если вы обновляете все три с помощью атомарных операций, это три независимые операции, поэтому читатель может увидеть структуру с одним значением, которое уже обновляется, и два не обновляются. Здесь вам понадобится блокировка, если вы должны убедиться, что читатель видит все значения в структуре как старые или новые значения.

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

Лучший способ избежать проблем с потоками - не делить данные между потоками. Если каждый поток обрабатывает большую часть времени с данными, к которым не имеет доступа ни один другой поток, вам вообще не понадобится блокировка этих данных (также без атомарных операций). Поэтому постарайтесь делиться как можно меньшим количеством данных между потоками. Тогда вам нужен только быстрый способ перемещения данных между потоками, если это действительно необходимо (ITC, Inter Thread Communication). В зависимости от вашей операционной системы, платформы и языка программирования (к сожалению, вы не сказали нам ни одного из них), могут существовать различные мощные методы для ITC.

И, наконец, еще один прием для работы с общими данными, но без какой-либо блокировки, состоит в том, чтобы убедиться, что потоки не обращаются к одним и тем же частям общих данных. Например. если два потока совместно используют массив, но один будет когда-либо иметь доступ только к четным, а другой только к нечетным индексам, вам не нужно блокировать. Или, если оба используют один и тот же блок памяти и один использует только верхнюю половину, а другой только нижний, блокировка не требуется. Хотя не сказано, что это приведет к хорошей производительности; особенно не на многоядерных процессорах. Операции записи одного потока в эти общие данные (работающие на одном ядре) могут вызвать сброс кеша для другого потока (работающего на другом ядре), и эти сбросы кеша часто являются узким местом для многопоточных приложений, работающих на современных многоядерных процессорах.

12 голосов
/ 18 сентября 2008

Написание многопоточного кода без блокировок - трудная задача; но эта статья от Херба Саттера поможет вам начать.

10 голосов
/ 09 апреля 2009

Как мой класс (Нир Шавит из «Искусства многопроцессорного программирования») сказал классу: пожалуйста, не надо Основная причина - тестируемость - вы не можете протестировать код синхронизации. Вы можете запустить симуляции, вы можете даже стресс-тест. Но это грубое приближение в лучшем случае. Что вам действительно нужно, так это математическое доказательство правильности. И очень немногие способны понять их, не говоря уже о написании их. Итак, как говорили другие: используйте существующие библиотеки. В блоге Джо Даффи рассматриваются некоторые приемы (раздел 28). Первое, что вы должны попробовать - это разбиение дерева - разбить на более мелкие задачи и объединить.

7 голосов
/ 18 сентября 2008

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

6 голосов
/ 09 апреля 2009

в ре. Ответ Сума, Мориса Эрлити, показывает в «Искусстве многопроцессорного программирования», что на самом деле все может быть написано без блокировок (см. Главу 6). iirc, это по существу включает в себя разделение задач на элементы узла обработки (например, закрытие функций) и постановку в очередь каждого из них. Потоки будут вычислять состояние, следуя всем узлам из последнего кэшированного. Очевидно, что в худшем случае это может привести к последовательной производительности, но оно имеет важные свойства без блокировки, предотвращая сценарии, в которых потоки могут планироваться на длительные периоды времени, когда они удерживают блокировки. Herlithy также достигает теоретической производительности без ожидания, а это означает, что один поток не будет ждать вечно, чтобы выиграть атомарную очередь (это много сложного кода).

Многопоточная очередь / стек на удивление сложна (проверьте проблему ABA ). Другие вещи могут быть очень простыми. Привыкнуть к блоку while (true) {atomicCAS, пока я его не поменял местами}; они невероятно мощные. Интуиция о том, что правильно с CAS, может помочь разработке, хотя вы должны использовать хорошее тестирование и, возможно, более мощные инструменты (возможно, SKETCH , предстоящий MIT Kendo или spin ?) чтобы проверить правильность, если вы можете уменьшить его до простой структуры.

Пожалуйста, опубликуйте больше о вашей проблеме. Трудно дать хороший ответ без подробностей.

edit неизменность хороша, но ее применимость ограничена, если я правильно понимаю. Это действительно не преодолевает опасности записи после чтения; рассмотрим два потока, выполняющих "mem = NewNode (mem)"; они оба могли читать мем, затем оба записывали его; не подходит для классической функции приращения. Кроме того, он, вероятно, медленный из-за выделения кучи (которая должна быть синхронизирована между потоками).

5 голосов
/ 18 сентября 2008

Неизменяемость будет иметь этот эффект. Изменения в объекте приводят к появлению нового объекта. Лисп работает таким образом под одеялом.

Пункт 13 из Эффективная Java объясняет эту технику.

4 голосов
/ 18 сентября 2008

В Cliff Click было проведено несколько крупных исследований структур данных без блокировки с использованием конечных автоматов, а также опубликовано множество реализаций для Java. Вы можете найти его статьи, слайды и реализации в его блоге: http://blogs.azulsystems.com/cliff/

2 голосов
/ 18 сентября 2008

Используйте существующую реализацию, так как эта область работы является областью экспертов в области предметной области и докторов наук (если вы хотите, чтобы это было сделано правильно!)

Например, здесь есть библиотека кода:

http://www.cl.cam.ac.uk/research/srg/netos/lock-free/

...