RxJavaを使用したバックプレッシャーへの対処
1. 概要
この記事では、RxJava libraryが背圧の処理にどのように役立つかを見ていきます。
簡単に言えば、RxJavaは、1つまたは複数のObserversがサブスクライブできるObservables,を導入することにより、リアクティブストリームの概念を利用します。 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)メソッドの呼び出しです。これは、Observerが消費できるアイテムでObservableがより早くいっぱいになる原因となる長時間実行タスクをエミュレートするために実行しています。それら。
Observables – HotとColdには2つのタイプがあり、背圧処理に関してはまったく異なります。
2.1. コールドObservables
コールドObservableは特定のアイテムのシーケンスを放出しますが、Observerが便利であると判断したときに、Observerが望むレートで、の整合性を損なうことなく、このシーケンスの放出を開始できます。シーケンス。 Cold Observable is providing items in a lazy way.
Observerは、そのアイテムを処理する準備ができている場合にのみ要素を取得します。アイテムはプル方式で要求されるため、Observableでバッファリングする必要はありません。
たとえば、100万から100万までの要素の静的範囲に基づいてObservableを作成すると、それらのアイテムが観察される頻度に関係なく、そのObservableは同じシーケンスのアイテムを放出します。
Observable.range(1, 1_000_000)
.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute);
プログラムを開始すると、アイテムはObserverによって遅延的に計算され、プル方式で要求されます。 Schedulers.computation()メソッドは、RxJava.の計算スレッドプール内でObserverを実行することを意味します
プログラムの出力は、Observableから1つずつ呼び出されたcompute()メソッドの結果で構成されます。
compute integer v: 1
compute integer v: 2
compute integer v: 3
compute integer v: 4
...
コールドObservablesはプル方式で機能するため、何らかの形の背圧をかける必要はありません。 コールドObservableによって発行されるアイテムの例には、データベースクエリ、ファイル取得、またはWeb要求の結果が含まれる場合があります。
2.2. ホットObservables
ホットObservableはアイテムの生成を開始し、作成されるとすぐにアイテムを放出します。 これは、処理のコールドObservablesプルモデルとは反対です。 Hot Observable emits items at its own pace, and it is up to its observers to keep up.
ObserverがObservableによって生成されるほど速くアイテムを消費できない場合、それらはメモリをいっぱいにし、最終的に%()を引き起こすため、バッファリングまたは他の方法で処理する必要があります。 t2)s
それらのアイテムを処理しているエンドユーザーに100万のアイテムを生産しているホットなObservable,の例を考えてみましょう。 Observerのcompute()メソッドがすべてのアイテムを処理するのに時間がかかると、Observableはメモリをアイテムでいっぱいにし始め、プログラムが失敗します。
PublishSubject source = PublishSubject.create();
source.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute, Throwable::printStackTrace);
IntStream.range(1, 1_000_000).forEach(source::onNext);
過剰生産のObservableを処理する方法を定義しなかったため、そのプログラムの実行はMissingBackpressureExceptionで失敗します。
ホット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に要素のバッチを送信することで、これを軽減できます。要素を1つずつではなく:
PublishSubject source = PublishSubject.create();
source.window(500)
.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute, Throwable::printStackTrace);
引数500,を指定してwindow()メソッドを使用すると、Observableに要素を500サイズのバッチにグループ化するように指示されます。 この手法により、Observerが要素を1つずつ処理する場合に比べて要素のバッチをより高速に処理できる場合に、Observableが過剰に生成される問題を減らすことができます。
5. 要素をスキップする
Observableによって生成された値の一部を安全に無視できる場合は、特定の時間内のサンプリングとスロットル演算子を使用できます。
メソッドsample()およびthrottleFirst()は、パラメーターとして期間を取ります。
-
sample()メソッドは、要素のシーケンスを定期的に調べ、パラメーターとして指定された期間内に生成された最後のアイテムを出力します。
-
throttleFirst()メソッドは、パラメーターとして指定された期間の後に生成された最初のアイテムを発行します
期間は、生成された要素のシーケンスから1つの特定の要素が選択されるまでの時間です。 要素をスキップすることにより、バックプレッシャーを処理する戦略を指定できます。
PublishSubject source = PublishSubject.create();
source.sample(100, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.computation())
.subscribe(ComputeFunction::compute, Throwable::printStackTrace);
要素をスキップする戦略はsample()メソッドになるように指定しました。 持続時間100ミリ秒のシーケンスのサンプルが必要です。 その要素はObserver.に放出されます
ただし、これらの演算子は、ダウンストリームのObserverによる値の受信率を低下させるだけであるため、MissingBackpressureExceptionにつながる可能性があることに注意してください。
6. 充填Observableバッファの処理
要素のサンプリングまたはバッチ処理の戦略がバッファー,の充填に役立たない場合は、バッファーがいっぱいになったときにケースを処理する戦略を実装する必要があります。
BufferOverflowException.を防ぐには、onBackpressureBuffer()メソッドを使用する必要があります
onBackpressureBuffer()メソッドは、Observableバッファーの容量、バッファーがいっぱいになったときに呼び出されるメソッド、およびバッファーから破棄する必要のある要素を処理するための戦略の3つの引数を取ります。 オーバーフローの戦略は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によって生成された最新のアイテムを追加することです。
最後の2つの戦略は、要素をドロップアウトするため、ストリームの不連続性を引き起こすことに注意してください。 さらに、BufferOverflowExceptionを通知しません。
7. 過剰生産された要素をすべて削除する
ダウンストリームのObserverが要素を受け取る準備ができていないときはいつでも、onBackpressureDrop()メソッドを使用してその要素をシーケンスから削除できます。
このメソッドは、戦略ON_OVERFLOW_DROP_LATEST.でバッファの容量をゼロに設定したonBackpressureBuffer()メソッドと考えることができます。
この演算子は、ソース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プロジェクトであるため、そのままインポートして実行するのは簡単です。