it-roy-ru.com

Буфер без круговой блокировки

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

теперь два вопроса:

  1. Является ли круговой буфер без блокировки ответом?

  2. Если да, то, прежде чем я сделаю свою собственную, знаете ли вы какую-либо публичную реализацию, которая будет соответствовать моим потребностям?

Любые указатели в реализации циклического буфера без блокировки всегда приветствуются.

Кстати, делать это в C++ на Linux.

Дополнительная информация:

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

Идея дизайна, к которой я склоняюсь, - это циклический буфер без полузамкнутости, в котором поток производителя помещает данные в буфер так быстро, как только может, давайте назовем заголовок буфера A без блокировки, если буфер не заполнен, когда A встречает конец буфера Z. Каждый потребительский поток будет содержать два указателя на кольцевой буфер, P и Pnгде P - локальная головка буфера потока, а P -n это n-ый элемент после P. Каждый потребительский поток продвинет свои P и Pn как только он заканчивает обработку текущего P и указатель конца буфера Z продвигается с самым медленным Pn, Когда P догоняет A, что означает, что больше нет новых обновлений для обработки, потребитель вращается и действительно занят, ожидая, пока A снова продвинется. Если потребительский поток вращается слишком долго, его можно перевести в спящий режим и ждать переменную условия, но я согласен с тем, что потребитель принимает цикл ЦП в ожидании обновления, потому что это не увеличивает мою задержку (у меня будет больше ядер ЦП чем темы). Представьте, что у вас есть круговая дорожка, и производитель работает перед группой потребителей, ключ в том, чтобы настроить систему так, чтобы производитель, как правило, работал на несколько шагов впереди потребителей, и большинство из этих операций могут быть сделано с использованием техники без блокировки. Я понимаю, что правильно понять детали реализации нелегко ... хорошо, очень сложно, поэтому я хочу учиться на чужих ошибках, прежде чем делать несколько своих.

68
Shing Yip

Я провел особое исследование структур данных без блокировок за последние пару лет. Я прочитал большинство статей в этой области (их всего около сорока или около того - хотя только около десяти или пятнадцати реально пригодились :-)

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

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

Однако я считаю, что есть решение по вашему требованию.

Вы должны связать очередь без блокировок со свободным списком без блокировок.

Свободный список даст вам предварительное распределение и, таким образом, устранит (финансово дорогое) требование для распределителя без блокировки; когда свободный список пуст, вы копируете поведение кольцевого буфера, мгновенно удаляя элемент из очереди и используя его вместо этого.

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

Майкл и Скотт разработали действительно хорошую очередь без блокировки еще в 1996 году. Ссылка ниже даст вам достаточно подробностей, чтобы отследить PDF их статьи; Майкл и Скотт, FIFO

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

35
user82238

Условием искусства для того, что вы хотите, является очередь без блокировки . Есть отличный набор заметок со ссылками на код и документы Росс Бенчина. Парень, работе которого я доверяю больше всего Морис Херлихи (для американцев он произносит свое имя как "Моррис").

32
Norman Ramsey

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

В (x86/x64) Linux внутрипотоковая синхронизация с использованием мьютексов достаточно дешевая, если нет конфликтов. Сконцентрируйтесь на минимизации времени, которое производители и потребители должны держать на своих замках. Учитывая, что вы сказали, что вам нужны только последние N записанных точек данных, я думаю, что циклический буфер будет делать это достаточно хорошо. Тем не менее, я не совсем понимаю, как это согласуется с требованием блокирования и с идеей потребителей, которые фактически потребляют (удаляют) данные, которые они читают. (Хотите ли вы, чтобы потребители смотрели только на последние N точек данных, а не удаляли их? Хотите, чтобы производители не заботились о том, что потребители не успевают, и просто перезаписывают старые данные?)

Кроме того, как прокомментировал Zan Lynx, вы можете агрегировать/буферизовать ваши данные в большие куски, когда их будет много. Вы можете буферизовать фиксированное количество точек или все данные, полученные в течение определенного периода времени. , Это означает, что будет меньше операций синхронизации. Тем не менее, это приводит к задержке, но если вы не используете Linux в реальном времени, то вам все равно придется с этим справляться.

11
Doug

Реализация в библиотеке Boost заслуживает рассмотрения. Это простой в использовании и довольно высокая производительность. Я написал тест и запустил его на четырехъядерном ноутбуке i7 (8 потоков) и получаю ~ 4M операций постановки/снятия в секунду. Еще одной реализацией, не упомянутой до сих пор, является очередь MPMC по адресу http://moodycamel.com/blog/2014/detailed-design-of-a-lock-free-queue . Я провел несколько простых испытаний этой реализации на одном ноутбуке с 32 производителями и 32 потребителями. Как рекламируется, это быстрее, чем увеличить очередь без блокировки.

Поскольку большинство других ответов утверждают, что безблокировочное программирование сложно Большинству реализаций будет сложно обнаружить критические случаи, которые требуют много тестирования и отладки для исправления. Они обычно исправляются с помощью осторожного размещения барьеров памяти в коде. Вы также найдете доказательства правильности, опубликованные во многих академических статьях. Я предпочитаю тестировать эти реализации с помощью инструмента грубой силы. Любой алгоритм без блокировки, который вы планируете использовать в производстве, следует проверить на корректность с помощью такого инструмента, как http://research.Microsoft.com/en-us/um/people/lamport/tla/tla.html ,.

5
Alex

Есть довольно хорошая серия статей об этом на DDJ . В качестве признака того, насколько сложным может быть этот материал, это исправление более ранняя статья , в котором это неправильно. Убедитесь, что вы понимаете ошибки, прежде чем делать свои собственные) -;

5
Henk Holterman

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

Однако недавно я заметил это видео: очередь без блокировки SPSC на основе кольцевого буфера

Это основано на высокопроизводительной библиотеке с открытым исходным кодом Java, называемой LMAX distruptor, используемой торговой системой: LMAX Distruptor

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

Ниже вы можете увидеть очень простую реализацию C++ 11 для него:

// USING SEQUENTIAL MEMORY
#include<thread>
#include<atomic>
#include <cinttypes>
using namespace std;

#define RING_BUFFER_SIZE 1024  // power of 2 for efficient %
class lockless_ring_buffer_spsc
{
    public :

        lockless_ring_buffer_spsc()
        {
            write.store(0);
            read.store(0);
        }

        bool try_Push(int64_t val)
        {
            const auto current_tail = write.load();
            const auto next_tail = increment(current_tail);
            if (next_tail != read.load())
            {
                buffer[current_tail] = val;
                write.store(next_tail);
                return true;
            }

            return false;  
        }

        void Push(int64_t val)
        {
            while( ! try_Push(val) );
            // TODO: exponential backoff / sleep
        }

        bool try_pop(int64_t* pval)
        {
            auto currentHead = read.load();

            if (currentHead == write.load())
            {
                return false;
            }

            *pval = buffer[currentHead];
            read.store(increment(currentHead));

            return true;
        }

        int64_t pop()
        {
            int64_t ret;
            while( ! try_pop(&ret) );
            // TODO: exponential backoff / sleep
            return ret;
        }

    private :
        std::atomic<int64_t> write;
        std::atomic<int64_t> read;
        static const int64_t size = RING_BUFFER_SIZE;
        int64_t buffer[RING_BUFFER_SIZE];

        int64_t increment(int n)
        {
            return (n + 1) % size;
        }
};

int main (int argc, char** argv)
{
    lockless_ring_buffer_spsc queue;

    std::thread write_thread( [&] () {
             for(int i = 0; i<1000000; i++)
             {
                    queue.Push(i);
             }
         }  // End of lambda expression
                                                );
    std::thread read_thread( [&] () {
             for(int i = 0; i<1000000; i++)
             {
                    queue.pop();
             }
         }  // End of lambda expression
                                                );
    write_thread.join();
    read_thread.join();

     return 0;
}
4
Akin Ocal

Очередь Саттера неоптимальна, и он это знает. Искусство многоядерного программирования - отличный пример, но не доверяйте Java ребятам в моделях памяти, и точка. Ссылки Росса не дадут вам однозначного ответа, потому что у них были свои библиотеки по таким проблемам и так далее.

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

4
rama-jka toti

Я согласен с эта статья и рекомендую не использовать структуры данных без блокировки. Относительно недавняя статья об очередях fifo без блокировки: this , поиск других статей того же автора (ов); есть также кандидатская диссертация Чалмерса относительно структур данных без блокировки (я потерял связь). Однако вы не сказали, насколько велики ваши элементы - структуры данных без блокировок эффективно работают только с элементами размера Word, поэтому вам придется динамически выделять элементы, если они больше, чем машинный Word (32 или 64). биты). Если вы динамически распределяете элементы, вы перемещаете узкое место (предположительно, так как вы не профилировали свою программу и в основном выполняете преждевременную оптимизацию) в распределитель памяти, поэтому вам нужен распределитель памяти без блокировки, например, Streamflow и интегрируйте его с вашим приложением.

4
zvrba

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

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

4
Nikolai Fetissov

Проверьте Disruptor ( Как его использовать ), который является кольцевым буфером, на который могут подписаться несколько потоков:

2
Rolf Kristensen

Хотя это старый вопрос, никто не упомянул --- безблокировочный кольцевой буфер DPDK . Это кольцевой буфер с высокой пропускной способностью, который поддерживает несколько производителей и нескольких потребителей. Он также предоставляет режимы с одним потребителем и одним производителем, а кольцевой буфер не требует ожидания в режиме SPSC. Он написан на C и поддерживает несколько архитектур.

Кроме того, он поддерживает режимы "Bulk" и "Burst", в которых элементы можно ставить в очередь/снимать в массовом порядке. Конструкция позволяет нескольким потребителям или нескольким производителям одновременно писать в очередь, просто зарезервировав пространство путем перемещения атомного указателя.

2
Saman Barghi

Это старый поток, но, поскольку он еще не упоминался - в среде JUCE C++ есть доступный без блокировок круговой 1 производитель -> 1 потребитель FIFO.

https://www.juce.com/doc/classAbstractFifo#details

2
Nikolay Tsenkov

Вот как я бы это сделал:

  • сопоставить очередь в массив
  • сохранить состояние при следующем чтении и следующей следующей записи индексов
  • держать пустой полный битовый вектор

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

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

Имейте в виду,

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

Просто для полноты: в OtlContainers есть хорошо протестированный кольцевой буфер без блокировки, но он написан на Delphi (TOmniBaseBoundedQueue - кольцевой буфер, а TOmniBaseBoundedStack - ограниченный стек). В том же модуле есть неограниченная очередь (TOmniBaseQueue). Неограниченная очередь описана в Динамическая очередь без блокировки - делает все правильно . Начальная реализация ограниченной очереди (кольцевой буфер) была описана в очередь без блокировки, наконец-то! , но с тех пор код был обновлен.

1
gabr

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

Рассмотрим этот абзац из LDD3:

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

0
Dražen G.

Вы можете попробовать lfqueue

Это простой в использовании, это круговой дизайн без блокировки

int *ret;

lfqueue_t results;

lfqueue_init(&results);

/** Wrap This scope in multithread testing **/
int_data = (int*) malloc(sizeof(int));
assert(int_data != NULL);
*int_data = i++;
/*Enqueue*/
while (lfqueue_enq(&results, int_data) != 1) ;

/*Dequeue*/
while ( (ret = lfqueue_deq(&results)) == NULL);

// printf("%d\n", *(int*) ret );
free(ret);
/** End **/

lfqueue_clear(&results);
0
Oktaheta