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, поэтому его легко импортировать и беги как есть.