深入理解 RxJava2:從 observeOn 到作用域(4)

蝶翼的罪發表於2018-08-26

前言

歡迎來到深入理解 RxJava2 系列第四篇。前一篇中我們認識了執行緒操作符,並詳細介紹了 subscribeOn 操作符,最後一個例子給大家介紹使用該操作符的注意事項,由於篇幅問題就戛然而止了。本文將繼續介紹 observeOn,並用這兩者做一些比較幫助大家深刻理解它們。

observeOn

前文我們提過subscribeOn是對上游起作用的,而observeOn恰恰相反是作用於下游的,因此從某種意義上說observeOn的功能更加強大與豐富。

方法描述

public final Flowable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize)
複製程式碼

scheduler

深入理解 RxJava2:從 observeOn 到作用域(4)

如上圖所示,scheduler在這裡起的作用就是排程任務,下游消費者的onNext / onComplete / onError均會在傳入目標scheduler中執行。

delayError

delayError 顧名思義,當出現錯誤時,是否會延遲onError的執行。

為什麼會出現這樣的情況,因為消費的方法均是在Scheduler中執行的,因此會有生產和消費速率不一致的情形。那麼當出現錯誤時,可能佇列裡還有資料未傳遞給下游,因此delayError這個引數就是為了解決這個問題。

delayEror預設為false, 當出現錯誤時會直接越過未消費的佇列中的資料,在下游處理完當前的資料後會立即執行onError,如下圖所示:

深入理解 RxJava2:從 observeOn 到作用域(4)

如果為true則會保持和上游一致的順序向下遊排程onNext,最後執行onError

bufferSize

這裡著重強調一下bufferSize這個引數,在FlowableObservableobserveOn中都有這個引數,但是在兩者中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,大家使用時請注意。

SpscLinkedArrayQueueSpscArrayQueue相似,但當佇列滿後會自動擴容,因此永遠也不會導致 MBE,但是可能會因為消費和生產的速度不一致導致 OOM。

這裡也呼應了筆者在《深入理解 RxJava2:前世今生(1)》 中提到過的FlowableObservable的差別。

作用域

上面我們提過,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後,後續操作符的方法都是執行在上游排程的執行緒。因此每個操作符所執行的執行緒都是由上游最近的一個observeOnScheduler決定。

因此筆者稱之為最近生效原則,但是請注意,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作用域內,則對應的多個資料來源均會受到影響,望大家注意。

交叉對比

最後我們再用一個例子,將observeOnsubscribeOn混合使用,驗證我們上面的結論:

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
複製程式碼

資料流的執行緒如下圖所示:

深入理解 RxJava2:從 observeOn 到作用域(4)

結語

observeOn作為 RxJava2 的核心實現自然不只是筆者上面說的那些內容。筆者有意的避開了原始碼,不希望同時將過多的概念灌輸給大家。事實上observeOn的原始碼中深度實現了所謂的Fusion這個隱晦的概念,這些深層次的原始碼分析留到這個系列的後期,筆者也會一一分享。

感覺大家的閱讀,歡迎關注筆者公眾號,可以第一時間獲取更新,同時歡迎留言溝通。

深入理解 RxJava2:從 observeOn 到作用域(4)

相關文章