Статьи

Поточные безопасные буферы Agrona

В этом блоге я продолжаю серию публикаций о библиотеке Agrona , рассказывающую о том, как мы предлагаем легкий доступ к свободной памяти для выполнения потоковых операций . Я должен, вероятно, предостеречь, прежде чем мы продолжим, что это довольно сложная тема, и я не пытаюсь объяснить такие понятия, как барьеры памяти — просто обрисовать в общих чертах возможности API.

Недостатки ByteBuffer

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

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

Буферы Агроны

Agrona предоставляет несколько буферных классов и интерфейсов для преодоления этих недостатков. Эти буферы используются библиотеками Aeron и SBE .

  1. DirectBuffer — интерфейс верхнего уровня, который обеспечивает возможность чтения значений из буфера.
  2. MutableDirectBuffer— расширяет DirectBufferоперации добавления для записи в буфер.
  3. AtomicBuffer— Нет, это не атомная электростанция MutableDirectBuffer! Этот интерфейс добавляет элементарные операции и семантику сравнения и обмена.
  4. UnsafeBuffer— реализация по умолчанию. Имя unsafe не подразумевает, что класс не должен использоваться, просто то, что использует его вспомогательная реализация sun.misc.Unsafe.

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

Упаковка памяти

Чтобы иметь возможность что-либо делать с буфером, вы должны указать ему, где должен начинаться буфер! Этот процесс называется обертыванием основной памяти. Все методы для обёртывания памяти вызываются wrapи возможно обёртывание byte[], ByteBufferили DirectBuffer. Вы также можете указать смещение и длину, чтобы обернуть структуры данных. Например вот как вы заверните byte[].

        final int offset = 0;
        final int length = 5;
        buffer.wrap(new byte[length], offset, length);

Существует еще одна опция для переноса — это адрес в ячейке памяти. В этом случае метод берет базовый адрес памяти и ее длину. Это для поддержки таких вещей, как память, выделенная посредством sun.misc.Unsafeили, например, вызова malloc. Вот пример использования Unsafe.

        final int length = 10;
        final long address = unsafe.allocateMemory(length);
        buffer.wrap(address, length);

Обтекание памяти также устанавливает емкость буфера, к которому можно получить доступ через capacity()метод.

Accessors

Итак, теперь у вас есть буфер памяти вне кучи, из которого вы можете читать и записывать в него. Соглашение состоит в том, что каждый получатель начинается со слова getи имеет суффикс с типом значения, которое вы пытаетесь получить. Вы должны предоставить адрес, чтобы сказать, где в буфере для чтения. Там также необязательный параметр порядка байтов. Если порядок байтов не указан, то будет использоваться собственный порядок машины. Вот пример того, как увеличивать long в начале буфера:

        final int address = 0;
        long value = buffer.getLong(address, ByteOrder.BIG_ENDIAN);
        value++;
        buffer.putLong(address, value, ByteOrder.BIG_ENDIAN);

Так же как и примитивные типы, можно получать и помещать байты из буферов. В этом случае буфер, который будет считан или передан, передается как параметр. Снова byte[], ByteBufferили DirectBufferподдерживается. Например, вот как вы должны читать данные в byte[].

        final int offsetInBuffer = 0;
        final int offsetInResult = 0;
        final int length = 5;
        final byte[] result = new byte[length];
        buffer.getBytes(offsetInBuffer, result, offsetInResult, length, result);

Параллельные операции

intи longзначения также могут быть прочитаны или записаны с семантикой упорядочения памяти. Методы с суффиксом Orderedгарантируют, что в конечном итоге они будут установлены на соответствующее значение, и это значение в конечном итоге будет видно из другого потока, выполняющего переменное чтение значения. Другими словами, putLongOrderedавтоматически выполняет барьер памяти магазина-магазина . get*Volatileи put*Volatileследуйте той же семантике упорядочения, что и чтение и запись в переменные, объявленные с ключевым словом volatile, как в Java.

Более сложные операции с памятью также возможны через AtomicBuffer. Например, есть элемент, compareAndSetLongкоторый будет атомарно устанавливать обновленное значение по данному индексу, учитывая, что существующее значение есть ожидаемое значение. getAndAddLongМетод является полностью атомным способом добавления по данному индексу.

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

Проверка границ

Проверка границ — одна из тех острых проблем и тем продолжающихся дебатов. Избегание проверок границ может привести к более быстрому коду, но может привести к возникновению ошибки и остановке JVM. Буферы Agrona дают вам возможность отключить проверку границ через свойство командной строки agrona.disable.bounds.checks, но проверка границ по умолчанию. Это означает, что их использование безопасно, но если профилирование приложения тестируемого кода определяет, что проверка границ является узким местом, то его можно удалить.

Выводы

Буферы Agrona позволяют нам легко использовать внешнюю память без ограничений, которые накладывают на нас существующие байтовые буферы Java. Мы продолжаем расширять библиотеку, которую можно скачать с maven central .

Спасибо Майку Баркеру, Алексу Уилсону, Бенджи Веберу, Юану Макгрегору, Мэтью Крэнману за помощь в рассмотрении этого поста в блоге.