Как работает шаблон прерывания LMAX?

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

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

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

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

Есть ли хорошие рекомендации для лучшего объяснения?

Ответ 1

В проекте Google Code ссылка на техническую документацию на реализацию кольцевого буфера, однако это немного сухая, академичная и жесткая работа для кого-то, кто хочет узнать, как это работает. Однако есть некоторые сообщения в блогах, которые начали объяснять внутренности более читаемым образом. Существует пояснение кольцевого буфера, являющееся ядром шаблона разрушителя, описание потребительских барьеров (часть, относящаяся к чтению от дезорганизатора) и некоторые

Ответ 2

Сначала мы хотели бы понять предлагаемую модель программирования.

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

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

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

Зависимость от читателя возникает из-за того, что читатель А может аннотировать запись, а читатель В зависит от этой аннотации. Например, A выполняет некоторые вычисления в записи и сохраняет результат в поле a в записи. A затем перейдите, и теперь B может прочитать запись и сохранить значение a A. Если читатель C не зависит от A, C не должен пытаться читать a.

Это действительно интересная модель программирования. Независимо от производительности, модель сама по себе может принести пользу многим приложениям.

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

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

setNewEntry(EntryPopulator);

interface EntryPopulator{ void populate(Entry existingEntry); }

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

И много усилий, чтобы избежать блокировки, CAS, даже барьера памяти (например, используйте переменную переменной нестабильной последовательности, если есть только одна запись)

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

Ответ 3

Мартин Фаулер написал статью о LMAX и шаблоне disruptor, LMAX Architecture, что может прояснить ее дальше.

Ответ 4

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

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

Буфер поддерживается массивом флагов (целочисленный массив) его длины, который описывает доступность слотов буфера (подробнее см. подробности). Массив получает доступ как java # AtomicIntegerArray, поэтому для этого объяснения вы можете также предположить, что он один.

Может быть любое количество производителей. Когда производитель хочет записать в буфер, генерируется длинное число (как при вызове AtomicLong # getAndIncrement, Disruptor фактически использует свою собственную реализацию, но работает так же). Позвольте называть это сгенерированным longproducCallId. Аналогичным образом, consumerCallId генерируется, когда потребительская ENDS считывает слот из буфера. Доступен самый последний доступ к потребителю.

(Если есть много потребителей, выбирается вызов с наименьшим идентификатором.)

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

(Если производительCallId больше, чем последний queryCallId + bufferSize, это означает, что буфер заполнен, а продюсер вынужден ждать по шине, пока не станет доступно место.)

Затем производителю присваивается слот в буфере на основе его callId (который является prducerCallId по модулю bufferSize, но так как bufferSize всегда имеет мощность 2 (ограничение, установленное при создании буфера), используемая операция активации - производительCallId и ( bufferSize - 1)). Затем можно изменить событие в этом слоте.

(Фактический алгоритм немного сложнее, включая кэширование недавнего consumerId в отдельной атомной ссылке для целей оптимизации.)

Когда событие было изменено, изменение "опубликовано". При публикации соответствующего слота в массиве флагов заполняется обновленный флаг. Значение флага - это номер цикла (производительCallId, деленный на bufferSize (опять же, поскольку bufferSize имеет мощность 2, фактическая операция - это сдвиг вправо).

Аналогичным образом может быть любое количество потребителей. Каждый раз, когда потребитель хочет получить доступ к буферу, генерируется consumerCallId (в зависимости от того, как потребители были добавлены в разрушитель, атом, используемый в генерации id, может быть разделен или разделен для каждого из них). Этот consumerCallId затем сравнивается с последним producentCallId, и если он меньше этих двух, читателю разрешено продвигаться вперед.

(Точно так же, если производительCallId является даже для customerCallId, это означает, что буфер является empety, и потребитель вынужден ждать. Способ ожидания определяется WaitStrategy при создании прерывания.)

Для индивидуальных потребителей (те, у которых есть собственный генератор идентификаторов), следующая вещь проверяется на возможность использования партии. Слоты в буфере проверяются по порядку от единицы, соответствующей customerCallId (индекс определяется таким же образом, как и для производителей), который соответствует последнему производителюCallId.

Они проверяются в цикле путем сравнения значения флага, записанного в массиве флагов, против значения флага, генерируемого для customerCallId. Если флаги совпадают, это означает, что производители, заполняющие слоты, совершили свои изменения. Если нет, цикл прерывается, и возвращается самая высокая переменная changeId. Слоты из ConsumerCallId, полученные в changeId, могут потребляться в пакетном режиме.

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

Ответ 5

Из этой статьи:

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

Память-барьеры трудно объяснить, и блог Trisha сделал наилучшую попытку на мой взгляд с этим сообщением: http://mechanitis.blogspot.com/2011/08/dissecting-disruptor-why-its-so-fast.html

Но если вы не хотите погружаться в детали низкого уровня, вы можете просто знать, что барьеры памяти в Java реализованы с помощью ключевого слова volatile или через java.util.concurrent.AtomicLong. Последовательности шаблона разрушения AtomicLong и передаются между производителями и потребителями через блокировки памяти вместо блокировок.

Мне легче понять концепцию с помощью кода, поэтому приведенный ниже код - это простой helloworld от CoralQueue, который представляет собой реализацию шаблона прерывания, выполненную CoralBlocks с которыми я являюсь аффилированным лицом. В приведенном ниже коде вы можете увидеть, как шаблон disruptor реализует пакетную обработку и как кольцевой буфер (т.е. Круговой массив) позволяет без мусора общаться между двумя потоками:

package com.coralblocks.coralqueue.sample.queue;

import com.coralblocks.coralqueue.AtomicQueue;
import com.coralblocks.coralqueue.Queue;
import com.coralblocks.coralqueue.util.MutableLong;

public class Sample {

    public static void main(String[] args) throws InterruptedException {

        final Queue<MutableLong> queue = new AtomicQueue<MutableLong>(1024, MutableLong.class);

        Thread consumer = new Thread() {

            @Override
            public void run() {

                boolean running = true;

                while(running) {
                    long avail;
                    while((avail = queue.availableToPoll()) == 0); // busy spin
                    for(int i = 0; i < avail; i++) {
                        MutableLong ml = queue.poll();
                        if (ml.get() == -1) {
                            running = false;
                        } else {
                            System.out.println(ml.get());
                        }
                    }
                    queue.donePolling();
                }
            }

        };

        consumer.start();

        MutableLong ml;

        for(int i = 0; i < 10; i++) {
            while((ml = queue.nextToDispatch()) == null); // busy spin
            ml.set(System.nanoTime());
            queue.flush();
        }

        // send a message to stop consumer...
        while((ml = queue.nextToDispatch()) == null); // busy spin
        ml.set(-1);
        queue.flush();

        consumer.join(); // wait for the consumer thread to die...
    }
}