Планировщики в RxJava

1. Обзор

В этой статье мы сосредоточимся на различных типах Schedulers , которые мы будем использовать при написании многопоточных программ, основанных на методах RxJava Observable’s subscribeOn и observeOn .

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

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

2. Поведение потоков по умолчанию

  • По умолчанию Rx является однопоточным ** , что означает, что Observable и цепочка операторов, которые мы можем применить к нему, будут уведомлять своих наблюдателей в том же потоке, в котором вызывается его метод subscribe () .

Методы observeOn и subscribeOn принимают в качестве аргумента Scheduler, который, как следует из названия, является инструментом, который мы можем использовать для планирования отдельных действий.

Мы создадим нашу реализацию Scheduler с помощью метода createWorker , который возвращает Scheduler.Worker . A worker принимает действия и выполняет их последовательно в одном потоке.

В некотором смысле, worker сам по себе _S duduler, но мы не будем называть его Scheduler_ , чтобы избежать путаницы.

2.1. Планирование действий

Мы можем запланировать работу для любого Scheduler , создав нового worker и запланировав некоторые действия:

Scheduler scheduler = Schedulers.immediate();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> result += "action");

Assert.assertTrue(result.equals("action"));

Затем действие ставится в очередь в потоке, которому назначен работник.

2.2. Отмена действия

Scheduler.Worker расширяет Subscription . Вызов метода unsubscribe для worker приведет к опустошению очереди и отмене всех отложенных задач. Мы можем видеть это на примере:

Scheduler scheduler = Schedulers.newThread();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> {
    result += "First__Action";
    worker.unsubscribe();
});
worker.schedule(() -> result += "Second__Action");

Assert.assertTrue(result.equals("First__Action"));

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

3. Schedulers.newThread

Этот планировщик просто запускает новый поток каждый раз, когда его запрашивают через subscribeOn () или observeOn () .

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

Observable.just("Hello")
  .observeOn(Schedulers.newThread())
  .doOnNext(s ->
    result2 += Thread.currentThread().getName()
  )
  .observeOn(Schedulers.newThread())
  .subscribe(s ->
    result1 += Thread.currentThread().getName()
  );
Thread.sleep(500);
Assert.assertTrue(result1.equals("RxNewThreadScheduler-1"));
Assert.assertTrue(result2.equals("RxNewThreadScheduler-2"));

Когда Worker завершен, поток просто завершается. Этот Scheduler можно использовать только в том случае, если задачи в целом: выполнение занимает много времени, но их очень мало, так что потоки вряд ли будут повторно использоваться вообще.

Scheduler scheduler = Schedulers.newThread();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> {
    result += Thread.currentThread().getName() + "__Start";
    worker.schedule(() -> result += "__worker__");
    result += "__End";
});
Thread.sleep(3000);
Assert.assertTrue(result.equals(
  "RxNewThreadScheduler-1__Start__End__worker__"));

Когда мы запланировали worker для NewThreadScheduler, мы увидели, что рабочий привязан к определенному потоку.

4. Schedulers.immediate

Schedulers.immediate - это специальный планировщик, который вызывает задачу в клиентском потоке блокирующим образом, а не асинхронно, и возвращает после завершения действия:

Scheduler scheduler = Schedulers.immediate();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> {
    result += Thread.currentThread().getName() + "__Start";
    worker.schedule(() -> result += "__worker__");
    result += "__End";
});
Thread.sleep(500);
Assert.assertTrue(result.equals(
  "main__Start__worker____End"));

Фактически, подписка на Observable через immediate Scheduler обычно имеет тот же эффект, что и вовсе не подписка на какой-либо конкретный __S __cheduler:

Observable.just("Hello")
  .subscribeOn(Schedulers.immediate())
  .subscribe(s ->
    result += Thread.currentThread().getName()
  );
Thread.sleep(500);
Assert.assertTrue(result.equals("main"));

5. Schedulers.trampoline

Trampoline Scheduler очень похож на immediate , потому что он также планирует задачи в том же потоке, эффективно блокируя.

Однако предстоящее задание будет выполнено после завершения всех ранее запланированных заданий:

Observable.just(2, 4, 6, 8)
  .subscribeOn(Schedulers.trampoline())
  .subscribe(i -> result += "" + i);
Observable.just(1, 3, 5, 7, 9)
  .subscribeOn(Schedulers.trampoline())
  .subscribe(i -> result += "" + i);
Thread.sleep(500);
Assert.assertTrue(result.equals("246813579"));

Immediate сразу же вызывает заданную задачу, тогда как trampoline ожидает завершения текущей задачи.

Trampoline ‘s worker выполняет каждую задачу в потоке, который запланировал первую задачу. Первый вызов schedule блокируется, пока очередь не будет очищена:

Scheduler scheduler = Schedulers.trampoline();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> {
    result += Thread.currentThread().getName() + "Start";
    worker.schedule(() -> {
        result += "__middleStart";
        worker.schedule(() ->
            result += "__worker__"
        );
        result += "__middleEnd";
    });
    result += "__mainEnd";
});
Thread.sleep(500);
Assert.assertTrue(result
  .equals("mainStart__mainEnd__middleStart__middleEnd__worker__"));

6. Schedulers.from

Schedulers внутренне более сложны, чем Executors из java.util.concurrent - поэтому была необходима отдельная абстракция.

Но поскольку они концептуально очень похожи, неудивительно, что есть оболочка, которая может превратить Executor в Scheduler с помощью фабричного метода from :

private ThreadFactory threadFactory(String pattern) {
    return new ThreadFactoryBuilder()
      .setNameFormat(pattern)
      .build();
}

@Test
public void givenExecutors__whenSchedulerFrom__thenReturnElements()
 throws InterruptedException {

    ExecutorService poolA = newFixedThreadPool(
      10, threadFactory("Sched-A-%d"));
    Scheduler schedulerA = Schedulers.from(poolA);
    ExecutorService poolB = newFixedThreadPool(
      10, threadFactory("Sched-B-%d"));
    Scheduler schedulerB = Schedulers.from(poolB);

    Observable<String> observable = Observable.create(subscriber -> {
      subscriber.onNext("Alfa");
      subscriber.onNext("Beta");
      subscriber.onCompleted();
    });;

    observable
      .subscribeOn(schedulerA)
      .subscribeOn(schedulerB)
      .subscribe(
        x -> result += Thread.currentThread().getName() + x + "__",
        Throwable::printStackTrace,
        () -> result += "__Completed"
      );
    Thread.sleep(2000);
    Assert.assertTrue(result.equals(
      "Sched-A-0Alfa__Sched-A-0Beta____Completed"));
}

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

7. Schedulers.io

Этот Scheduler похож на newThread за исключением того факта, что уже запущенные потоки перерабатываются и могут обрабатывать будущие запросы.

Эта реализация работает аналогично ThreadPoolExecutor из java.util.concurrent с неограниченным пулом потоков. Каждый раз, когда запрашивается новый worker , либо запускается новый поток (а затем он некоторое время простаивает), либо повторно используется:

Observable.just("io")
  .subscribeOn(Schedulers.io())
  .subscribe(i -> result += Thread.currentThread().getName());

Assert.assertTrue(result.equals("RxIoScheduler-2"));

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

На практике следование Schedulers.io почти всегда является лучшим выбором.

8. Schedulers.computation

Computation S _cheduler по умолчанию ограничивает число параллельно работающих потоков значением availableProcessors () , как указано в служебном классе Runtime.getRuntime () _ .

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

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

Observable.just("computation")
  .subscribeOn(Schedulers.computation())
  .subscribe(i -> result += Thread.currentThread().getName());
Assert.assertTrue(result.equals("RxComputationScheduler-1"));

Если по какой-то причине нам нужно количество потоков, отличное от значения по умолчанию, мы всегда можем использовать системное свойство rx.scheduler.max-computation-threads .

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

9. Schedulers.test

Этот Scheduler используется только для целей тестирования, и мы никогда не увидим его в рабочем коде. Основным его преимуществом является возможность опережать часы, имитируя время, проходящее произвольно:

List<String> letters = Arrays.asList("A", "B", "C");
TestScheduler scheduler = Schedulers.test();
TestSubscriber<String> subscriber = new TestSubscriber<>();

Observable<Long> tick = Observable
  .interval(1, TimeUnit.SECONDS, scheduler);

Observable.from(letters)
  .zipWith(tick, (string, index) -> index + "-" + string)
  .subscribeOn(scheduler)
  .subscribe(subscriber);

subscriber.assertNoValues();
subscriber.assertNotCompleted();

scheduler.advanceTimeBy(1, TimeUnit.SECONDS);
subscriber.assertNoErrors();
subscriber.assertValueCount(1);
subscriber.assertValues("0-A");

scheduler.advanceTimeTo(3, TimeUnit.SECONDS);
subscriber.assertCompleted();
subscriber.assertNoErrors();
subscriber.assertValueCount(3);
assertThat(
  subscriber.getOnNextEvents(),
  hasItems("0-A", "1-B", "2-C"));

10. Планировщики по умолчанию

Некоторые операторы Observable в RxJava имеют альтернативные формы, которые позволяют нам указать, какой Scheduler оператор будет использовать для своей работы. Другие не работают с каким-либо конкретным Scheduler или оперируют с конкретным Scheduler по умолчанию.

Например, оператор delay принимает восходящие события и толкает их вниз по истечении заданного времени. Очевидно, что он не может содержать исходный поток в течение этого периода, поэтому он должен использовать другой Scheduler :

ExecutorService poolA = newFixedThreadPool(
  10, threadFactory("Sched1-"));
Scheduler schedulerA = Schedulers.from(poolA);
Observable.just('A', 'B')
  .delay(1, TimeUnit.SECONDS, schedulerA)
  .subscribe(i -> result+= Thread.currentThread().getName() + i + " ");

Thread.sleep(2000);
Assert.assertTrue(result.equals("Sched1-A Sched1-B "));

Без предоставления собственного schedulerA , все операторы ниже delay будут использовать computation Scheduler

Другими важными операторами, которые поддерживают пользовательские Schedulers , являются buffer, interval , range , timer , skip , take , timeout и несколько других. Если мы не предоставляем Scheduler для таких операторов, используется computation планировщик, который в большинстве случаев является безопасным по умолчанию.

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

В действительно реактивных приложениях, для которых все длительные операции являются асинхронными, очень мало потоков и, следовательно, Schedulers .

Мастеринг планировщиков важен для написания масштабируемого и безопасного кода с использованием RxJava. Разница между subscribeOn и observeOn особенно важна при высокой нагрузке, когда каждая задача должна выполняться именно тогда, когда мы ожидаем.

И последнее, но не менее важное: мы должны быть уверены, что Schedulers , используемый в нисходящем направлении, может идти в ногу со временем. Для получения дополнительной информации, есть эта статья о ссылке:/rxjava-backpressure[backpressure].

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