Параллельность с LMAX Disruptor - Введение

1. Обзор

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

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

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

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

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

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

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

Некоторые ориентировочные показатели стоимости кеша отсутствуют:

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

2.2. Почему не очереди

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

Они очень редко работают в сбалансированном среднем положении, где скорость производства и потребления равномерно совпадают.

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

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

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

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

ссылка:/uploads/RingBuffer-1-300x169.jpg%20300w[]

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

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

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

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

3. Использование библиотеки Disruptor

3.1. Maven Dependency

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

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.3.6</version>
</dependency>

Последняя версия зависимости может быть проверена here ,

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

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

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

   //standard getters and setters
}

EventFactory позволяет Disruptor предварительно распределять события.

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

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

public class SingleEventPrintConsumer {
    ...

    public EventHandler<ValueEvent>[]getEventHandler() {
        EventHandler<ValueEvent> 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<ValueEvent> disruptor
  = new Disruptor<>(
    ValueEvent.EVENT__FACTORY,
    16,
    threadFactory,
    ProducerType.SINGLE,
    waitStrategy);

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

  • Event Factory - отвечает за создание объектов, которые будут

хранится в кольцевом буфере во время инициализации ** Размер кольцевого буфера - мы определили 16 как размер кольца

буфер. Это должно быть степень 2, иначе будет выброшено исключение инициализация. Это важно, потому что легко выполнить большую часть операции с использованием логических бинарных операторов, например мод операции ** Thread Factory - Фабрика для создания потоков для обработчиков событий

  • Тип производителя - указывает, будет ли у нас один или несколько

производители ** Стратегия ожидания - Определяет, как мы хотели бы справиться с медленным абонентом

кто не поспевает за темпом продюсера

Подключите потребительский обработчик:

disruptor.handleEventsWith(getEventHandler());

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

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

Чтобы запустить Disruptor:

RingBuffer<ValueEvent> ringBuffer = disruptor.start();

3.6. Производство и публикация событий

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

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

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

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

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

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

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