Scheduler in RxJava

Scheduler in RxJava

1. Überblick

In diesem Artikel konzentrieren wir uns auf verschiedene Arten vonSchedulers, die wir beim Schreiben von Multithreading-Programmen verwenden, die auf den Methoden RxJavaObservable’s subscribeOn undobserveOnbasieren.

Schedulers bieten die Möglichkeit anzugeben, wo und wann Aufgaben im Zusammenhang mit dem Betrieb einerObservable-Kette ausgeführt werden sollen.

Wir können einScheduler aus den in der KlasseSchedulers. beschriebenen Factory-Methoden erhalten

2. Standard-Threading-Verhalten

By default,Rx is single-threaded, was bedeutet, dass einObservable und die Kette von Operatoren, die wir darauf anwenden können, seine Beobachter über denselben Thread benachrichtigen, auf dem seinesubscribe()-Methode aufgerufen wird.

Die MethodenobserveOn undsubscribeOn verwenden als Argument aScheduler,, das, wie der Name schon sagt, ein Werkzeug ist, mit dem wir einzelne Aktionen planen können.

Wir erstellen unsere Implementierung vonScheduler mithilfe dercreateWorker-Methode, dieScheduler.Worker. zurückgibt. Aworker akzeptiert Aktionen und führt sie nacheinander in einem einzelnen Thread aus.

In gewisser Weise ist einworker selbst einScheduler, aber wir werden ihn nicht alsScheduler bezeichnen, um Verwirrung zu vermeiden.

2.1. Planen einer Aktion

Wir können einen Job für alleScheduler planen, indem wir neueworker erstellen und einige Aktionen planen:

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

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

Die Aktion wird dann in dem Thread in die Warteschlange gestellt, dem der Worker zugewiesen ist.

2.2. Aktion abbrechen

Scheduler.Worker erweitertSubscription. Wenn Sie dieunsubscribe-Methode fürworker aufrufen, wird die Warteschlange geleert und alle ausstehenden Aufgaben werden abgebrochen. Wir können das am Beispiel sehen:

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"));

Die zweite Task wird nie ausgeführt, da die vorherige den gesamten Vorgang abgebrochen hat. Aktionen, die gerade ausgeführt werden, werden unterbrochen.

3. Schedulers.newThread

Dieser Scheduler startet einfach jedes Mal einen neuen Thread, wenn er übersubscribeOn() oderobserveOn() angefordert wird.

Es ist kaum eine gute Wahl, nicht nur wegen der Latenz beim Starten eines Threads, sondern auch, weil dieser Thread nicht wiederverwendet wird:

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"));

WennWorker fertig ist, wird der Thread einfach beendet. DieseScheduler können nur verwendet werden, wenn Aufgaben grobkörnig sind: Die Ausführung nimmt viel Zeit in Anspruch, es gibt jedoch nur sehr wenige, sodass Threads wahrscheinlich überhaupt nicht wiederverwendet werden.

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_"));

Als wirworker aufNewThreadScheduler, planten, sahen wir, dass der Worker an einen bestimmten Thread gebunden war.

4. Schedulers.immediate

Schedulers.immediate ist ein spezieller Scheduler, der eine Aufgabe innerhalb des Client-Threads blockierend und nicht asynchron aufruft und nach Abschluss der Aktion zurückgibt:

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"));

Tatsächlich hat das Abonnieren einesObservable überimmediate Scheduler normalerweise den gleichen Effekt wie das Nicht-Abonnieren eines bestimmtenS-Planers überhaupt:

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

5. Schedulers.trampoline

trampolineScheduler istimmediate sehr ähnlich, da es auch Aufgaben im selben Thread plant und effektiv blockiert.

Die bevorstehende Aufgabe ist jedoch executed, wenn alle zuvor geplanten Aufgaben abgeschlossen sind:

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 ruft sofort eine bestimmte Aufgabe auf, währendtrampoline darauf wartet, dass die aktuelle Aufgabe beendet wird.

Dietrampolineworker führen jede Aufgabe in dem Thread aus, der die erste Aufgabe geplant hat. Der erste Aufruf vonschedule wird blockiert, bis die Warteschlange geleert ist:

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 sind intern komplexer alsExecutors vonjava.util.concurrent - daher war eine separate Abstraktion erforderlich.

Da sie sich konzeptionell sehr ähnlich sind, gibt es nicht überraschend einen Wrapper, derExecutor mit der Factory-Methode vonfrominScheduler umwandeln kann:

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 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 wird für einen kurzen Zeitraum verwendet, plant jedoch kaum eine neue Aktion fürschedulerA, die die gesamte Arbeit erledigt. Somit werden mehreresubscribeOn methodsnicht nur ignoriert, sondern verursachen auch einen geringen Overhead.

7. Schedulers.io

DiesesScheduler ähnelt demnewThread, außer dass bereits gestartete Threads recycelt werden und möglicherweise zukünftige Anforderungen verarbeiten können.

Diese Implementierung funktioniert ähnlich wieThreadPoolExecutor vonjava.util.concurrent mit einem unbegrenzten Pool von Threads. Jedes Mal, wenn ein neuesworker angefordert wird, wird entweder ein neuer Thread gestartet (und später einige Zeit im Leerlauf gehalten) oder der inaktive Thread wird wiederverwendet:

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

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

Wir müssen mit unbegrenzten Ressourcen jeglicher Art vorsichtig sein - bei langsamen oder nicht reagierenden externen Abhängigkeiten wie Webdiensten könnenioscheduler eine enorme Anzahl von Threads starten, was dazu führt, dass unsere eigene Anwendung nicht mehr reagiert .

In der Praxis ist es fast immer besser,Schedulers.io zu folgen.

8. Schedulers.computation

Computation Scheduler begrenzt standardmäßig die Anzahl der Threads, die parallel zum Wert vonavailableProcessors() ausgeführt werden, wie in der DienstprogrammklasseRuntime.getRuntime() angegeben.

Wir sollten also eincomputation scheduler verwenden, wenn Aufgaben vollständig CPU-gebunden sind. Das heißt, sie benötigen Rechenleistung und haben keinen Blockierungscode.

Es wird eine unbeschränkte Warteschlange vor jedem Thread verwendet. Wenn die Aufgabe geplant ist, aber alle Kerne belegt sind, wird sie in die Warteschlange gestellt. Die Warteschlange kurz vor jedem Thread wächst jedoch weiter:

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

Wenn wir aus irgendeinem Grund eine andere Anzahl von Threads als die Standardanzahl benötigen, können wir immer die Systemeigenschaftrx.scheduler.max-computation-threadsverwenden.

Indem wir weniger Threads verwenden, können wir sicherstellen, dass immer ein oder mehrere CPU-Kerne im Leerlauf sind und selbst unter hoher Last der Thread-Pool voncomputationden Server nicht überlastet. Es ist einfach nicht möglich, mehr Berechnungsthreads als Kerne zu haben.

9. Schedulers.test

DiesesScheduler wird nur zu Testzwecken verwendet und wird im Produktionscode nie angezeigt. Sein Hauptvorteil ist die Möglichkeit, die Uhr vorzustellen und die vorbeiziehende Zeit willkürlich zu simulieren:

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

Observable 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. Standard-Scheduler

EinigeObservable-Operatoren in RxJava haben alternative Formen, mit denen wir festlegen können, welcheScheduler der Operator für seine Operation verwendet. Andere arbeiten nicht mit bestimmtenScheduler oder mit bestimmten StandardScheduler.

Beispielsweise nimmt der Operatordelay Upstream-Ereignisse auf und schiebt sie nach einer bestimmten Zeit stromabwärts. Offensichtlich kann es den ursprünglichen Thread während dieses Zeitraums nicht halten, daher muss ein anderesScheduler verwendet werden:

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 "));

Ohne Angabe eines benutzerdefiniertenschedulerA würden alle Operatoren unterdelaycomputation Scheduler verwenden.

Andere wichtige Operatoren, die benutzerdefinierteSchedulers unterstützen, sindbuffer,interval,range,timer,skip,take,timeout) s und mehrere andere. Wenn wir solchen Operatoren keinScheduler zur Verfügung stellen, wird der Scheduler voncomputationverwendet, was in den meisten Fällen ein sicherer Standard ist.

11. Fazit

In wirklich reaktiven Anwendungen, für die alle lang laufenden Vorgänge asynchron sind, werden nur sehr wenige Threads und damitSchedulers benötigt.

Das Beherrschen von Schedulern ist unerlässlich, um mit RxJava skalierbaren und sicheren Code zu schreiben. Der Unterschied zwischensubscribeOn undobserveOn ist besonders wichtig bei hoher Last, bei der jede Aufgabe genau dann ausgeführt werden muss, wenn wir es erwarten.

Last but not least müssen wir sicher sein, dassSchedulers, die stromabwärts verwendet werden, mit load generated by Schedulers upstream mithalten können. Für weitere Informationen gibt es diesen Artikel überbackpressure.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.