Рубрики
Без рубрики

Параллелизм с разрушителем LMAX – Введение

Краткое введение в LMAS Disruptor и использование API Disruptor

Автор оригинала: baeldung.

1. Обзор

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

2. Что такое Разрушитель?

Disruptor-это библиотека Java с открытым исходным кодом, написанная Максом. Это платформа параллельного программирования для обработки большого количества транзакций с низкой задержкой (и без сложностей параллельного кода). Оптимизация производительности достигается за счет разработки программного обеспечения, которое использует эффективность базового оборудования.

2.1. Механическая симпатия

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

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

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

Некоторые ориентировочные цифры стоимости пропусков кэша:

~60-80 нс Множественный Основная память
~15 нс ~40-45 циклов Кэш L3
~3 нс ~10 циклов Кэш L2
~1 нс ~3-4 цикла Кэш L1
Очень очень быстро 1 цикл Зарегистрировать

2.2. Почему Нет Очередей

Реализации очередей, как правило, имеют конфликт записи в переменных head, tail и size. Очереди, как правило, всегда близки к заполнению или почти пусты из-за различий в темпах между потребителями и производителями. Они очень редко работают в условиях сбалансированного среднего уровня, когда темпы производства и потребления равны.

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

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

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

2.3. Как работает разрушитель

Обзор кольцевого буфера и его API

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

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

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

3. Использование библиотеки разрушителей

3.1. Зависимость Maven

Давайте начнем с добавления зависимости библиотеки Disruptor в pom.xml :


    com.lmax
    disruptor
    3.3.6

Последнюю версию зависимости можно проверить здесь .

3.2. Определение события

Давайте определим событие, которое несет данные:

public static class ValueEvent {
    private int value;
    public final static EventFactory EVENT_FACTORY 
      = () -> new ValueEvent();

    // standard getters and setters
}

Фабрика событий позволяет разрушителю предварительно распределить события.

3.3. Потребитель

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

public class SingleEventPrintConsumer {
    ...

    public EventHandler[] getEventHandler() {
        EventHandler eventHandler 
          = (event, sequence, endOfBatch) 
            -> print(event.getValue(), sequence);
        return new EventHandler[] { eventHandler };
    }
 
    private void print(int id, long sequenceId) {
        logger.info("Id is " + id 
          + " sequence id that was used is " + sequenceId);
    }
}

В нашем примере потребитель просто печатает в журнал.

3.4. Построение разрушителя

Построить разрушитель:

ThreadFactory threadFactory = DaemonThreadFactory.INSTANCE;

WaitStrategy waitStrategy = new BusySpinWaitStrategy();
Disruptor disruptor 
  = new Disruptor<>(
    ValueEvent.EVENT_FACTORY, 
    16, 
    threadFactory, 
    ProducerType.SINGLE, 
    waitStrategy);

В конструкторе Disruptor определены следующие параметры:

  • Фабрика событий – Отвечает за создание объектов, которые будут храниться в кольцевом буфере во время инициализации
  • Размер кольцевого буфера – Мы определили 16 как размер кольцевого буфера. Это должно быть в степени 2, иначе при инициализации возникнет исключение. Это важно, потому что большинство операций легко выполнять с помощью логических двоичных операторов, например, mod operation
  • Фабрика потоков – Фабрика для создания потоков для обработчиков событий
  • Тип производителя – Указывает, будет ли у нас один или несколько производителей
  • Стратегия ожидания – Определяет, как мы хотели бы обращаться с медленным подписчиком, который не поспевает за темпом производителя

Подключите обработчик потребителя:

disruptor.handleEventsWith(getEventHandler());

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

3.5. Запуск разрушителя

Чтобы запустить разрушитель:

RingBuffer ringBuffer = disruptor.start();

3.6. Подготовка и публикация мероприятий

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

Используйте RingBuffer from Disruptor для публикации:

for (int eventCount = 0; eventCount < 32; eventCount++) {
    long sequenceId = ringBuffer.next();
    ValueEvent valueEvent = ringBuffer.get(sequenceId);
    valueEvent.setValue(eventCount);
    ringBuffer.publish(sequenceId);
}

Здесь производитель производит и публикует элементы последовательно. Здесь важно отметить, что Disruptor работает аналогично протоколу 2-фазной фиксации. Он считывает новый идентификатор последовательности и публикует. В следующий раз он должен получить идентификатор последовательности + 1 в качестве следующего идентификатора последовательности.

4. Заключение

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

Пример кода можно найти в проекте GitHub – это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.