Lock Free Queue - один производитель, несколько потребителей - PullRequest
17 голосов
/ 24 апреля 2010

Я ищу метод для реализации структуры данных очереди без блокировки, которая поддерживает одного производителя и нескольких потребителей. Я посмотрел на классический метод Магеда Майкла и Майкла Скотта (1996), но их версия использует связанные списки. Я хотел бы реализацию, которая использует ограниченный круговой буфер. Что-то, что использует атомарные переменные?

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

Я пытаюсь кодировать это на C / C ++, используя библиотеку pthread на 64-битной архитектуре Intel.

Спасибо, Shirish

Ответы [ 4 ]

9 голосов
/ 24 апреля 2010

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

Когда я реализовал очередь MS в C ++, я создал распределитель без блокировки, используя стек, который очень легко реализовать. Если у меня MSQueue, то во время компиляции я знаю sizeof (MSQueue :: node). Затем я делаю стек из N буферов необходимого размера. N может расти, то есть, если pop () возвращает null, можно просто запросить кучу дополнительных блоков, и они помещаются в стек. Вне возможного блокирующего вызова для дополнительной памяти это операция без блокировки.

Обратите внимание, что T не может иметь нетривиальный dtor. Я работал над версией, которая позволяла использовать нетривиальные dtors, которая действительно работала. Но я обнаружил, что было проще просто сделать T указателем на T, который я хотел, когда производитель освободил собственность, а потребитель приобрел собственность. Это, конечно, требует, чтобы сам T выделялся с использованием методов lockfree, но тот же распределитель, который я сделал со стеком, работает и здесь.

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

  1. без блокировки делает меня независимым от планировщика. Программирование на основе блокировок зависит от планировщика, чтобы убедиться, что держатели блокировки работают, чтобы они могли снять блокировку. Это то, что вызывает «инверсию приоритетов» В Linux есть некоторые атрибуты блокировки, чтобы убедиться, что это происходит
  2. Если я независим от планировщика, ОС намного проще управлять временными срезами, и я получаю гораздо меньше переключений контекста
  3. проще писать правильные многопоточные программы, используя методы lockfree, поскольку мне не нужно беспокоиться о взаимоблокировках, livelock, планировании, синхронизации и т. Д. Это особенно верно для реализаций разделяемой памяти, где процесс может умереть, удерживая блокировку в разделяемой памяти, и нет возможности снять блокировку
  4. методы без блокировки гораздо проще масштабировать. На самом деле, я реализовал методы без блокировки, используя обмен сообщениями по сети. Распределенные блокировки, подобные этому, являются кошмаром

Тем не менее, во многих случаях предпочтительны и / или требуются методы, основанные на блокировке

  1. при обновлении вещей, которые дорого или невозможно скопировать. Большинство методов без блокировок используют какое-то управление версиями, то есть делают копию объекта, обновляют его и проверяют, остается ли общая версия такой же, как и при копировании, а затем обновляете текущую версию. Els скопируйте его снова, примените обновление и проверьте снова. Продолжайте делать это, пока это не сработает. Это хорошо, когда объекты маленькие, но если они большие или содержат файловые дескрипторы и т. Д., Тогда не рекомендуется
  2. К большинству типов невозможно получить доступ без блокировки, например, любой контейнер STL. Они имеют инварианты, которые требуют неатомарного доступа, например assert (vector.size () == vector.end () - vector.begin ()). Поэтому, если вы обновляете / считываете общий вектор, его нужно заблокировать.
3 голосов
/ 06 декабря 2011

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

Этот сайт: http://www.1024cores.net

Предоставляет некоторые действительно полезные структуры данных без блокировки / без ожидания с подробными объяснениями.

То, что вы ищете, - это решение проблемы чтения / записи без блокировки.

См .: http://www.1024cores.net/home/lock-free-algorithms/reader-writer-problem

1 голос
/ 24 апреля 2010

Для традиционного одноблочного кругового буфера я думаю, что это просто невозможно сделать безопасно с атомарными операциями. Вы должны сделать так много за одно чтение. Предположим, у вас есть структура, которая имеет это:

uint8_t* buf;
unsigned int size; // Actual max. buffer size
unsigned int length; // Actual stored data length (suppose in write prohibited from being > size)
unsigned int offset; // Start of current stored data

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

  1. Проверьте, не превышает ли считанная длина сохраненную длину
  2. Проверьте, не превышают ли смещение + длина чтения границы буфера
  3. Считать данные
  4. Увеличение смещения, уменьшение длины

Что вам, безусловно, нужно сделать синхронизированным (таким атомарным), чтобы эта работа работала? На самом деле объедините шаги 1 и 4 в один атомарный шаг, или чтобы уточнить: сделать это синхронизировано:

  1. проверьте read_length, это может быть что-то вроде read_length=min(read_length,length);
  2. уменьшение длины с read_length: length-=read_length
  3. получить локальную копию со смещения unsigned int local_offset = offset
  4. увеличить смещение с длиной чтения: offset+=read_length

После этого вы можете просто сделать memcpy (или что-то еще), начиная с вашего local_offset, проверить, превышает ли ваше чтение размер буфера по кругу (разделенный на 2 memcpy), .... Это «вполне» потокобезопасный, ваш метод записи все еще может записывать поверх памяти, которую вы читаете, поэтому убедитесь, что ваш буфер действительно достаточно большой, чтобы минимизировать эту возможность.

Теперь, хотя я могу представить, что вы можете комбинировать 3 и 4 (я думаю, это то, что они делают в случае со связанным списком) или даже 1 и 2 в атомарных операциях, я не вижу, чтобы вы делали всю эту сделку в одной атомарной операции :).

Однако вы можете отказаться от проверки длины, если ваши потребители очень умны и всегда будут знать, что читать. Тогда вам также понадобится новая переменная woffset, потому что старый метод (offset + length)% size для определения смещения записи больше не будет работать. Обратите внимание, что это близко к случаю связанного списка, где вы фактически всегда читаете один элемент (= фиксированный, известный размер) из списка. Также здесь, если вы сделаете это круговым связанным списком, вы можете читать много или писать в позицию, которую вы читаете в данный момент!

Наконец: мой совет, просто используйте блокировку, я использую класс CircularBuffer, абсолютно безопасный для чтения и записи) для видеопотока в режиме реального времени 720p60, и у меня вообще не возникает проблем со скоростью из-за блокировки.

0 голосов
/ 01 ноября 2018

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

Может быть более одного решения, но здесь есть реализация: https://github.com/tudinfse/FFQ В документе конференции, указанном в файле readme, подробно описан алгоритм.

...