前言
歡迎來到深入理解 RxJava2 系列第四篇。前一篇中我們認識了執行緒操作符,並詳細介紹了 subscribeOn 操作符,最後一個例子給大家介紹使用該操作符的注意事項,由於篇幅問題就戛然而止了。本文將繼續介紹 observeOn,並用這兩者做一些比較幫助大家深刻理解它們。
observeOn
前文我們提過subscribeOn
是對上游起作用的,而observeOn
恰恰相反是作用於下游的,因此從某種意義上說observeOn
的功能更加強大與豐富。
方法描述
public final Flowable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize)
複製程式碼
scheduler
如上圖所示,scheduler
在這裡起的作用就是排程任務,下游消費者的onNext / onComplete / onError
均會在傳入目標scheduler
中執行。
delayError
delayError
顧名思義,當出現錯誤時,是否會延遲onError
的執行。
為什麼會出現這樣的情況,因為消費的方法均是在Scheduler
中執行的,因此會有生產和消費速率不一致的情形。那麼當出現錯誤時,可能佇列裡還有資料未傳遞給下游,因此delayError
這個引數就是為了解決這個問題。
delayEror
預設為false
, 當出現錯誤時會直接越過未消費的佇列中的資料,在下游處理完當前的資料後會立即執行onError
,如下圖所示:
如果為true
則會保持和上游一致的順序向下遊排程onNext
,最後執行onError
。
bufferSize
這裡著重強調一下bufferSize
這個引數,在Flowable
與Observable
的observeOn
中都有這個引數,但是在兩者中bufferSize
的效果是完全不一樣的,因為選擇的資料結構不一樣:
- Flowable:
queue = new SpscArrayQueue<T>(bufferSize)
- Observable:
queue = new SpscLinkedArrayQueue<T>(bufferSize)
SpscXXXQueue
上述的兩種佇列均是 RxJava 中提供的無鎖的單生產者單消費者的佇列,是 Fast Flow 和 BQueue 在 Java 中的實現,用以提升 RxJava 資料流的吞吐量。關於細節我們不再贅述,有興趣的讀者可以自己去搜尋。
但是在上面兩個佇列中,SpscArrayQueue
是一個固定長度快取的佇列,當佇列滿了時繼續入隊,Flowable 會丟擲MissingBackpressureException
。此外還有一個小細節,實際快取的長度大於等於傳入值的 2 的冪。例如傳入 20 會變成 32,而傳入 32 則還是 32,大家使用時請注意。
SpscLinkedArrayQueue
與SpscArrayQueue
相似,但當佇列滿後會自動擴容,因此永遠也不會導致 MBE,但是可能會因為消費和生產的速度不一致導致 OOM。
這裡也呼應了筆者在《深入理解 RxJava2:前世今生(1)》 中提到過的Flowable
與Observable
的差別。
作用域
上面我們提過,observeOn
是對下游生效的,一個簡單的例子:
Flowable.just(1).observeOn(Schedulers.io())
.subscribe(i -> {
System.out.println(Thread.currentThread().getName());
});
輸出:
RxCachedThreadScheduler-1
複製程式碼
但是當有多個操作符,且存在多次observeOn
時,每個方法都是執行在什麼執行緒呢?
Flowable.just(1).observeOn(Schedulers.io())
.map(i -> {
System.out.println(Thread.currentThread().getName());
return i;
})
.observeOn(Schedulers.computation())
.subscribe(i -> {
System.out.println(Thread.currentThread().getName());
});
輸出:
RxCachedThreadScheduler-1
RxComputationThreadPool-1
複製程式碼
這裡就涉及到一些 RxJava 實現的細節,多數操作符是基於上游呼叫onNext / onComplete / onError
的進一步封裝,在不涉及包含Scheduler
的操作符的情況下,在上游呼叫了observeOn
後,後續操作符的方法都是執行在上游排程的執行緒。因此每個操作符所執行的執行緒都是由上游最近的一個observeOn
的Scheduler
決定。
因此筆者稱之為最近生效原則,但是請注意,observeOn
是影響下游的,因此操作符所執行的執行緒受的是最近上游的observeOn
影響,切莫記反了。
示例
因此在實際使用中靈活的使用observeOn
,使得程式碼的效率最大化。這裡筆者再舉個例子:
Flowable.just(new File("input.txt"))
.map(f -> new BufferedReader(new InputStreamReader(new FileInputStream(f))))
.observeOn(Schedulers.io())
.flatMap(r -> Flowable.<String, BufferedReader>generate(() -> r, (br, e) -> {
String s = br.readLine();
if (s != null) {
e.onNext(s);
} else {
System.out.println(Thread.currentThread().getName());
e.onComplete();
}
}, BufferedReader::close))
.observeOn(Schedulers.computation())
.map(Integer::parseInt)
.reduce(0, (total, item) -> {
System.out.println(item);
return total + item;
})
.subscribe(s -> {
System.out.println("total: " + s);
System.out.println(Thread.currentThread().getName());
});
輸出:
RxCachedThreadScheduler-1
1
2
3
4
5
total: 15
RxComputationThreadPool-1
複製程式碼
如上程式碼所示,我們從 input.txt 讀出每行的字串,然後轉成一個 int, 最後求和。這裡我們靈活地使用了兩次observeOn
,在讀檔案時,排程至IoScheduler
,隨後做計算工作時排程至ComputationScheduler
,從控制檯的輸出可以見執行緒的的確確是我們所期望的。當然這裡求和只是一個示例,讀者們可以舉一反三。
事實上上面的程式碼還不是最優的:
Flowable.just(new File("input.txt"))
.map(f -> new BufferedReader(new InputStreamReader(new FileInputStream(f))))
.observeOn(Schedulers.io())
.flatMap(r -> Flowable.<String, BufferedReader>generate(() -> r, (br, e) -> {
String s = br.readLine();
if (s != null) {
e.onNext(s);
} else {
System.out.println(Thread.currentThread().getName());
e.onComplete();
}
}, BufferedReader::close))
.parallel()
.runOn(Schedulers.computation())
.map(Integer::parseInt)
.reduce((i, j) -> {
System.out.println(Thread.currentThread().getName());
return i + j;
})
.subscribe(s -> {
System.out.println("total: " + s);
System.out.println(Thread.currentThread().getName());
});
輸出:
RxCachedThreadScheduler-1
RxComputationThreadPool-1
RxComputationThreadPool-2
RxComputationThreadPool-4
RxComputationThreadPool-4
total: 15
RxComputationThreadPool-4
複製程式碼
如上程式碼所示我們可以充分利用多核的效能,通過parallel
來並行運算,當然這裡用在求和就有點殺雞用牛刀的意思了,筆者這裡只是一個舉例。更多 parallel 相關的內容,留待後續分享。
subscribeOn
回到正題,事實上subscribeOn
同樣遵循最近生效原則,但是與observeOn
恰恰相反。操作符會被最近的下游的subscribeOn
排程,因為subscribeOn
影響的是上游。
但是和observeOn
又有一些微妙的差別在於,我們通常呼叫subscribeOn
更加關注最上游的資料來源的執行緒。因此通常不會在中間過程中呼叫多次,任意的呼叫一次subscribeOn
均會影響上游所有操作符的subscribe
所在的執行緒,且不受observeOn
的影響。這是由於這兩者機制的不同,subscribeOn
是將整個上游的subscribe
方法都排程到目標執行緒了。
多資料來源
但是在一些特別的情況下subscribeOn
多次的使用也是有意義的,尤其是上游有多個資料來源時。多資料來源也就是存在超過一個Publisher
的操作符,如:zipWith / takeUntil / amb
,如果此類操作符如果在subscribeOn
作用域內,則對應的多個資料來源均會受到影響,望大家注意。
交叉對比
最後我們再用一個例子,將observeOn
與subscribeOn
混合使用,驗證我們上面的結論:
Flowable.<Integer>create(t -> {
System.out.println(Thread.currentThread().getName());
t.onNext(1);
t.onComplete();
}, BackpressureStrategy.BUFFER)
.observeOn(Schedulers.io())
.map(i -> {
System.out.println(Thread.currentThread().getName());
return i;
})
.subscribeOn(Schedulers.newThread())
.observeOn(Schedulers.computation())
.subscribe(i -> {
System.out.println(Thread.currentThread().getName());
});
輸出:
RxNewThreadScheduler-1
RxCachedThreadScheduler-1
RxComputationThreadPool-1
複製程式碼
資料流的執行緒如下圖所示:
結語
observeOn
作為 RxJava2 的核心實現自然不只是筆者上面說的那些內容。筆者有意的避開了原始碼,不希望同時將過多的概念灌輸給大家。事實上observeOn
的原始碼中深度實現了所謂的Fusion
這個隱晦的概念,這些深層次的原始碼分析留到這個系列的後期,筆者也會一一分享。
感覺大家的閱讀,歡迎關注筆者公眾號,可以第一時間獲取更新,同時歡迎留言溝通。