Работа с противодавлением с помощью RxJava

Работа с противодавлением с помощью RxJava

1. обзор

В этой статье мы рассмотрим, какRxJava library помогает нам справляться с противодавлением.

Проще говоря, RxJava использует концепцию реактивных потоков, вводяObservables,, на которые может подписаться один или несколькоObservers. Dealing with possibly infinite streams is very challenging, as we need to face a problem of a backpressure.

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

2. ГорячийObservables против холодногоObservables

Во-первых, давайте создадим простую функцию-получатель, которая будет использоваться в качестве потребителя элементов изObservables, которые мы определим позже:

public class ComputeFunction {
    public static void compute(Integer v) {
        try {
            System.out.println("compute integer v: " + v);
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Наша функцияcompute() просто печатает аргумент. Здесь важно отметить вызов методаThread.sleep(1000) - мы делаем это для имитации некоторой длительной задачи, которая заставитObservable заполниться элементами быстрее, чемObserver может потреблять их.

У нас есть два типаObservables – Hot иCold - они совершенно разные, когда дело доходит до обработки противодавления.

2.1. ХолодныйObservables

ХолодныйObservable испускает определенную последовательность элементов, но может начать испускать эту последовательность, когда егоObserver сочтет это удобным, и с любой скоростью, которую пожелаетObserver, без нарушения целостности последовательность. Cold Observable is providing items in a lazy way.

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

Например, если вы создаетеObservable на основе статического диапазона элементов от одного до одного миллиона, этотObservable будет выдавать одну и ту же последовательность элементов независимо от того, как часто эти элементы наблюдаются:

Observable.range(1, 1_000_000)
  .observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute);

Когда мы запускаем нашу программу, элементы будут вычислятьсяObserver лениво и будут запрашиваться в режиме pull. МетодSchedulers.computation() означает, что мы хотим запустить нашObserver в пуле вычислительных потоков вRxJava.

Вывод программы будет состоять из результата методаcompute(), вызываемого для одного элемента за другим изObservable:

compute integer v: 1
compute integer v: 2
compute integer v: 3
compute integer v: 4
...

ХолодныеObservablesне нуждаются в каком-либо противодавлении, потому что они работают в режиме вытягивания. Примеры элементов, выдаваемых холоднымObservable, могут включать результаты запроса к базе данных, извлечение файла или веб-запрос.

2.2. ГорячийObservables

ГорячийObservable начинает генерировать элементы и сразу же излучает их, когда они создаются. Это противоречит модели обработки ColdObservables pull. Hot Observable emits items at its own pace, and it is up to its observers to keep up.

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

Давайте рассмотрим пример hotObservable,, который производит 1 миллион товаров для конечного потребителя, который обрабатывает эти товары. Когда методуcompute() вObserver требуется некоторое время для обработки каждого элемента,Observable начинает заполнять память элементами, вызывая сбой программы:

PublishSubject source = PublishSubject.create();

source.observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute, Throwable::printStackTrace);

IntStream.range(1, 1_000_000).forEach(source::onNext);

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

Примеры элементов, генерируемых горячимObservable, могут включать события мыши и клавиатуры, системные события или цены на акции.

3. Буферизация перепроизводстваObservable

Первый способ справиться с перепроизводствомObservable - определить какой-то буфер для элементов, которые не могут быть обработаныObserver.

Мы можем сделать это, вызвав методbuffer():

PublishSubject source = PublishSubject.create();

source.buffer(1024)
  .observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute, Throwable::printStackTrace);

Определение буфера размером 1024 дастObserver некоторое время, чтобы догнать источник перепроизводства. В буфере будут храниться элементы, которые еще не были обработаны.

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

Однако обратите внимание, что обычноthis may be only a temporary fix, поскольку переполнение все еще может произойти, если источник превышает прогнозируемый размер буфера.

4. Пакетирование отправленных элементов

Мы можем пакетно перепроизводить элементы в окнах из N элементов.

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

PublishSubject source = PublishSubject.create();

source.window(500)
  .observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute, Throwable::printStackTrace);

Использование методаwindow() с аргументом500, скажетObservable сгруппировать элементы в пакеты размером 500. Этот метод может уменьшить проблему перепроизводстваObservable, когдаObserver может обрабатывать партию элементов быстрее, чем обрабатывая элементы один за другим.

5. Пропуск элементов

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

Методыsample() иthrottleFirst() принимают продолжительность в качестве параметра:

  • Метод sample() периодически просматривает последовательность элементов и выдает последний элемент, который был создан в течение времени, указанного в качестве параметра.

  • МетодthrottleFirst() испускает первый элемент, созданный по истечении времени, указанного в качестве параметра.

Продолжительность - это время, после которого один определенный элемент выбирается из последовательности произведенных элементов. Мы можем указать стратегию обработки противодавления, пропустив элементы:

PublishSubject source = PublishSubject.create();

source.sample(100, TimeUnit.MILLISECONDS)
  .observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute, Throwable::printStackTrace);

Мы указали, что стратегией пропуска элементов будет методsample(). Нам нужен образец последовательности продолжительностью 100 миллисекунд. Этот элемент будет отправлен вObserver.

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

6. Работа с заполнением буфераObservable

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

Нам нужно использовать методonBackpressureBuffer(), чтобы предотвратитьBufferOverflowException.

МетодonBackpressureBuffer() принимает три аргумента: емкость буфераObservable, метод, который вызывается при заполнении буфера, и стратегию обработки элементов, которые необходимо удалить из буфера. Стратегии переполнения относятся к классуBackpressureOverflow.

Существует 4 типа действий, которые могут быть выполнены при заполнении буфера:

  • ON_OVERFLOW_ERROR – это поведение по умолчанию, сигнализирующееBufferOverflowException, когда буфер заполнен

  • ON_OVERFLOW_DEFAULT – в настоящее время совпадает сON_OVERFLOW_ERROR

  • ON_OVERFLOW_DROP_LATEST - если произойдет переполнение, текущее значение будет просто проигнорировано, и только старые значения будут доставлены после запросаObserver ниже по течению

  • ON_OVERFLOW_DROP_OLDEST - удаляет самый старый элемент в буфере и добавляет к нему текущее значение

Давайте посмотрим, как указать эту стратегию:

Observable.range(1, 1_000_000)
  .onBackpressureBuffer(16, () -> {}, BackpressureOverflow.ON_OVERFLOW_DROP_OLDEST)
  .observeOn(Schedulers.computation())
  .subscribe(e -> {}, Throwable::printStackTrace);

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

Обратите внимание, что последние две стратегии вызывают разрыв в потоке, поскольку они выпадают из элементов. Кроме того, они не будут сигнализироватьBufferOverflowException.

7. Удаление всех перепроизводимых элементов

Когда нижестоящийObserver не готов принять элемент, мы можем использовать методonBackpressureDrop(), чтобы удалить этот элемент из последовательности.

Мы можем думать об этом методе как о методеonBackpressureBuffer() с емкостью буфера, равной нулю, со стратегиейON_OVERFLOW_DROP_LATEST.

Этот оператор полезен, когда мы можем безопасно игнорировать значения из источникаObservable (такие как движения мыши или текущие сигналы местоположения GPS), поскольку позже будут более актуальные значения:

Observable.range(1, 1_000_000)
  .onBackpressureDrop()
  .observeOn(Schedulers.computation())
  .doOnNext(ComputeFunction::compute)
  .subscribe(v -> {}, Throwable::printStackTrace);

МетодonBackpressureDrop() устраняет проблему перепроизводстваObservable, но его следует использовать с осторожностью.

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

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

Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.