RxJavaのスケジューラ

RxJavaのスケジューラー

1. 概要

この記事では、RxJavaObservable’s subscribeOnおよびobserveOnメソッドに基づくマルチスレッドプログラムの作成に使用するさまざまなタイプのSchedulersに焦点を当てます。

Schedulersは、Observableチェーンの操作に関連するタスクをいつどこで実行するかを指定する機会を提供します。

クラスSchedulers.で記述されたファクトリメソッドからSchedulerを取得できます。

2. デフォルトのスレッディング動作

By default,Rx is single-threadedは、Observableとそれに適用できる演算子のチェーンが、subscribe()メソッドが呼び出されたのと同じスレッドでオブザーバーに通知することを意味します。

observeOnメソッドとsubscribeOnメソッドは、引数としてScheduler,を取ります。これは、名前が示すように、個々のアクションをスケジュールするために使用できるツールです。

createWorkerメソッドを使用してSchedulerの実装を作成します。このメソッドは、Scheduler.Worker.を返します。workerはアクションを受け入れ、単一のスレッドで順番に実行します。

ある意味で、workerはそれ自体がSスケジューラーですが、混乱を避けるためにSchedulerとは呼びません。

2.1. アクションのスケジューリング

新しいworkerを作成し、いくつかのアクションをスケジュールすることで、任意のSchedulerでジョブをスケジュールできます。

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

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

アクションは、ワーカーが割り当てられているスレッドのキューに入れられます。

2.2. アクションのキャンセル

Scheduler.WorkerSubscriptionを拡張します。 workerunsubscribeメソッドを呼び出すと、キューが空になり、保留中のすべてのタスクがキャンセルされます。 例によってそれを見ることができます:

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

2番目のタスクは、前のタスクが操作全体をキャンセルしたため実行されません。 実行中のアクションは中断されます。

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

NewThreadScheduler,workerをスケジュールすると、ワーカーが特定のスレッドにバインドされていることがわかりました。

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

実際、immediate Schedulerを介してObservableにサブスクライブすると、通常、特定のSスケジューラーにサブスクライブしないのと同じ効果があります。

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

5. Schedulers.trampoline

trampolineSchedulerは、同じスレッドでタスクをスケジュールし、効果的にブロックするため、immediateと非常によく似ています。

ただし、以前にスケジュールされたすべてのタスクが完了すると、次のタスクはexecutedになります。

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は現在のタスクが終了するのを待ちます。

trampolineworkerは、最初のタスクをスケジュールしたスレッド上のすべてのタスクを実行します。 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は、java.util.concurrentExecutorsよりも内部的に複雑であるため、個別の抽象化が必要でした。

ただし、概念的には非常に似ているため、当然のことながら、fromファクトリメソッドを使用してExecutorSchedulerに変換できるラッパーがあります。

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は短期間使用されますが、すべての作業を実行するschedulerAに対する新しいアクションをスケジュールすることはほとんどありません。 したがって、複数のsubscribeOn methodsは無視されるだけでなく、小さなオーバーヘッドが発生します。

7. Schedulers.io

このSchedulerは、すでに開始されたスレッドがリサイクルされ、将来の要求を処理できる可能性があることを除いて、newThreadと同様です。

この実装は、スレッドの無制限のプールを持つjava.util.concurrentThreadPoolExecutorと同様に機能します。 新しいworkerが要求されるたびに、新しいスレッドが開始されるか(後でしばらくアイドル状態が維持されます)、アイドル状態のスレッドが再利用されます。

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

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

あらゆる種類の無制限のリソースに注意する必要があります。Webサービスのような遅いまたは応答しない外部依存関係の場合、ioschedulerが膨大な数のスレッドを開始し、独自のアプリケーションが応答しなくなる可能性があります。

実際には、Schedulers.ioに従うことは、ほとんどの場合、より良い選択です。

8. Schedulers.computation

Computation Schedulerは、デフォルトで、Runtime.getRuntime()ユーティリティクラスにあるように、availableProcessors()の値と並行して実行されるスレッドの数を制限します。

したがって、タスクが完全にCPUにバインドされている場合は、computation schedulerを使用する必要があります。つまり、計算能力が必要であり、ブロッキングコードはありません。

すべてのスレッドの前に無制限のキューを使用するため、タスクがスケジュールされているが、すべてのコアが占有されている場合、キューに入れられます。 ただし、各スレッドの直前のキューは成長し続けます。

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

何らかの理由で、デフォルトとは異なる数のスレッドが必要な場合は、常にrx.scheduler.max-computation-threadsシステムプロパティを使用できます。

使用するスレッドの数を減らすことで、常に1つ以上のCPUコアがアイドル状態になり、負荷が高い場合でも、computationスレッドプールがサーバーを飽和させないようにすることができます。 コアよりも多くの計算スレッドを持つことは不可能です。

9. Schedulers.test

このSchedulerはテスト目的でのみ使用され、本番コードでは表示されません。 その主な利点は、任意の時間の経過をシミュレートするクロックを進める機能です。

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. デフォルトのスケジューラ

RxJavaの一部のObservable演算子には、演算子がその操作に使用する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,intervalrangetimerskiptaketimeoutです。 )s、および他のいくつか。 このような演算子にSchedulerを提供しない場合、computationスケジューラーが使用されます。これは、ほとんどの場合、安全なデフォルトです。

11. 結論

すべての長時間実行操作が非同期である真にリアクティブなアプリケーションでは、スレッドが非常に少ないため、Schedulersが必要です。

マスタリングスケジューラは、RxJavaを使用してスケーラブルで安全なコードを記述するために不可欠です。 subscribeOnobserveOnの違いは、すべてのタスクを期待どおりに正確に実行する必要がある高負荷の下で特に重要です。

最後になりましたが、ダウンストリームで使用されるSchedulersがload generated by Schedulers upstreamに追いつくことができることを確認する必要があります。 詳細については、backpressureに関するこの記事があります。

これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。