深入理解 RxJava 的執行緒模型

鳥窩發表於2017-01-30

ReactiveX是Reactive Extensions的縮寫,一般簡寫為Rx,最初是LINQ的一個擴充套件,由微軟的架構師Erik Meijer領導的團隊開發,在2012年11月開源,Rx是一個程式設計模型,目標是提供一致的程式設計介面,幫助開發者更方便的處理非同步資料流,Rx庫支援.NET、JavaScript和C++,Rx近幾年越來越流行了,現在已經支援幾乎全部的流行程式語言了,Rx的大部分語言庫由ReactiveX這個組織負責維護,比較流行的有RxJava/RxJS/Rx.NET,社群網站是 reactivex.io

Netflix參考微軟的Reactive Extensions建立了Java的實現RxJava,主要是為了簡化伺服器端的併發。2013年二月份,Ben Christensen 和 Jafar Husain發在Netflix技術部落格的一篇文章第一次向世界展示了RxJava。

RxJava也在Android開發中得到廣泛的應用。

ReactiveX

An API for asynchronous programming with observable streams.

A combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming.

雖然RxJava是為非同步程式設計實現的庫,但是如果不清楚它的使用,或者錯誤地使用了它的執行緒排程,反而不能很好的利用它的非同步程式設計提到系統的處理速度。本文通過例項演示錯誤的RxJava的使用,解釋RxJava的執行緒排程模型,主要介紹SchedulerobserveOnsubscribeOn的使用。

本文中的例子以併發傳送http request請求為基礎,通過效能檢驗RxJava的執行緒排程。

第一個例子,效能超好?

我們首先看第一個例子:

  public static void testRxJavaWithoutBlocking(int count) throws Exception {
    CountDownLatch finishedLatch = new CountDownLatch(1);
    long t = System.nanoTime();
    Observable.range(0, count).map(i -> {
        //System.out.println("A:" + Thread.currentThread().getName());
        return 200;
    }).subscribe(statusCode -> {
        //System.out.println("B:" + Thread.currentThread().getName());
    }, error -> {
    }, () -> {
        finishedLatch.countDown();
    });
    finishedLatch.await();
    t = (System.nanoTime() - t) / 1000000; //ms
    System.out.println("RxJavaWithoutBlocking TPS: " + count * 1000 / t);
}

這個例子是一個基本的RxJava的使用,利用Range建立一個Observable, subscriber處理接收的資料。因為整個邏輯沒有阻塞,程式執行起來很快,

輸出結果為:

RxJavaWithoutBlocking TPS: 7692307

2 加上業務的模擬,效能超差

上面的例子是一個理想化的程式,沒雨任何阻塞。我們模擬一下實際的應用,加上業務處理。

業務邏輯是傳送一個http的請求,httpserver是一個模擬器,針對每個請求有30毫秒的延遲。subscriber統計請求結果:

public static void testRxJavaWithBlocking(int count) throws Exception {
        URL url = new URL("http://127.0.0.1:8999/");
        CountDownLatch finishedLatch = new CountDownLatch(1);
        long t = System.nanoTime();
        Observable.range(0, count).map(i -> {
            try {
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                int responseCode = conn.getResponseCode();
                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    //response.append(inputLine);
                }
                in.close();
                return responseCode;
            } catch (Exception ex) {
                return -1;
            }
        }).subscribe(statusCode -> {
        }, error -> {
        }, () -> {
            finishedLatch.countDown();
        });
        finishedLatch.await();
        t = (System.nanoTime() - t) / 1000000; //ms
        System.out.println("RxJavaWithBlocking TPS: " + count * 1000 / t);
    }

執行結果如下:

RxJavaWithBlocking TPS: 29。

@#¥%%……&!

效能怎麼突降呢,第一個例子看起來效能超好啊,http server只增加了一個30毫秒的延遲,導致這個方法每秒只能處理29個請求。

如果我們估算一下, 29*30= 870 毫秒,大約1秒,正好和單個執行緒傳送處理所有的請求的TPS差不多。

後面我們也會看到,實際的確是一個執行緒處理的,你可以在程式碼中加入

3 加上排程器,不起作用?

如果你對subscribeOnobserveOn方法有些印象的話,可能會嘗試使用排程器去解決:

public static void testRxJavaWithBlocking(int count) throws Exception {
        URL url = new URL("http://127.0.0.1:8999/");
        CountDownLatch finishedLatch = new CountDownLatch(1);
        long t = System.nanoTime();
        Observable.range(0, count).map(i -> {
            try {
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                int responseCode = conn.getResponseCode();
                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    //response.append(inputLine);
                }
                in.close();
                return responseCode;
            } catch (Exception ex) {
                return -1;
            }
        }).subscribeOn(Schedulers.io()).observeOn(Schedulers.computation()).subscribe(statusCode -> {
        }, error -> {
        }, () -> {
            finishedLatch.countDown();
        });
        finishedLatch.await();
        t = (System.nanoTime() - t) / 1000000; //ms
        System.out.println("RxJavaWithBlocking TPS: " + count * 1000 / t);
    }

加上.subscribeOn(Schedulers.io()).observeOn(Schedulers.computation())看一下效能:

RxJavaWithBlocking TPS: 30

效能沒有改觀,是時候瞭解一下RxJava執行緒排程的問題了。

4 RxJava的執行緒模型

首先,依照Observable Contract, onNext是順序執行的,不會同時由多個執行緒併發執行。

預設情況下,它是在呼叫subscribe方法的那個執行緒中執行的。如第一個例子和第二個例子,Rx的操作和訊息接收處理都是在同一個執行緒中執行的。一旦由阻塞,比如第二個例子,久會導致這個執行緒被阻塞,吞吐量下降。

但是subscribeOn可以改變Observable的執行執行緒。

上圖中可以看到,如果你使用了subscribeOn方法,則Rx的執行將會切換到另外的執行緒上,而不是預設的呼叫執行緒。

需要注意的是,如果在Observable鏈中呼叫了多個subscribeOn方法,無論呼叫點在哪裡,Observable鏈只會使用第一個subscribeOn指定的排程器,正所謂”一見傾情”。

但是onNext還是順序執行的,所以第二個例子的效能依然低下。

observeOn可以中途改變Observable鏈的執行緒。前面說了,subscribeOn方法改變的源Observable的整個的執行執行緒,要想中途切換執行緒,就需要observeOn方法。

官方的一個簡略晦澀的解釋如下:

The SubscribeOn operator changes this behavior by specifying a different Scheduler on which the Observable should operate. The ObserveOn operator specifies a different Scheduler that the Observable will use to send notifications to its observers.

一圖勝千言:

注意箭頭的顏色和橫軸的顏色,不同的顏色代表不同的執行緒。

5 Schedulers

上面我們瞭解了RxJava可以使用subscribeOnobserveOn可以改變和切換執行緒,以及onNext是順序執行的,不是併發執行,至多也就切換到另外一個執行緒,如果它中間的操作是阻塞的,久會影響整個Rx的執行。

Rx是通過排程器來選擇哪個執行緒執行的,RxJava內建了幾種排程器,分別為不同的case提供執行緒:

  • io() : 這個排程器時用於I/O操作, 它可以增長或縮減來確定執行緒池的大小它是使用CachedThreadScheduler來實現的。需要注意的是,它的執行緒池是無限制的,如果你使用了大量的執行緒的話,可能會導致OutOfMemory等資源用盡的異常。
  • computation() : 這個是計算工作預設的排程器,它與I/O操作無關。它也是許多RxJava方法的預設排程器:buffer(),debounce(),delay(),interval(),sample(),skip()。

因為這些方法內部已經呼叫的排程器,所以你再呼叫subscribeOn是無效的,比如下面的例子總是使用computation排程器的執行緒。

Observable.just(1,2,3)
                .delay(1, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.newThread())
                .map(i -> {
                    System.out.println("map: " + Thread.currentThread().getName());
                    return i;
                })
                .subscribe(i -> {});
  • immediate() :這個排程器允許你立即在當前執行緒執行你指定的工作。它是timeout(),timeInterval(),以及timestamp()方法預設的排程器。
  • newThread() :建立一個新的執行緒只從。
  • trampoline() :為當前執行緒建立一個佇列,將當前任務加入到佇列中依次執行。

同時,Schedulers還提供了from靜態方法,使用者可以定製執行緒池:

ExecutorService es = Executors.newFixedThreadPool(200, new ThreadFactoryBuilder().setNameFormat("SubscribeOn-%d").build());
Schedulers.from(es)

6 改造,非同步執行

現在,我們已經瞭解了RxJava的執行緒執行,以及相關的排程器。可以看到上面的例子還是順序阻塞執行的,即使是切換到另外的執行緒上,依然是順序阻塞執行,顯示它的吞吐率非常非常的低。下一步我們就要改造這個例子,讓它能非同步的執行。

下面是一種改造方案,我先把程式碼貼出來,再解釋:

public static void testRxJavaWithFlatMap(int count) throws Exception {
    ExecutorService es = Executors.newFixedThreadPool(200, new ThreadFactoryBuilder().setNameFormat("SubscribeOn-%d").build());
    URL url = new URL("http://127.0.0.1:8999/");
    CountDownLatch finishedLatch = new CountDownLatch(1);
    long t = System.nanoTime();
    Observable.range(0, count).subscribeOn(Schedulers.io()).flatMap(i -> {
                //System.out.println("A: " + Thread.currentThread().getName());
                return Observable.just(i).subscribeOn(Schedulers.from(es)).map(v -> {
                            //System.out.println("B: " + Thread.currentThread().getName());
                            try {
                                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                                conn.setRequestMethod("GET");
                                int responseCode = conn.getResponseCode();
                                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                                String inputLine;
                                while ((inputLine = in.readLine()) != null) {
                                    //response.append(inputLine);
                                }
                                in.close();
                                return responseCode;
                            } catch (Exception ex) {
                                return -1;
                            }
                        }
                );
            }
    ).observeOn(Schedulers.computation()).subscribe(statusCode -> {
        //System.out.println("C: " + Thread.currentThread().getName());
    }, error -> {
    }, () -> {
        finishedLatch.countDown();
    });
    finishedLatch.await();
    t = (System.nanoTime() - t) / 1000000; //ms
    System.out.println("RxJavaWithFlatMap TPS: " + count * 1000 / t);
    es.shutdownNow();
}

通過flatmap可以將源Observable的元素項轉成n個Observable,生成的每個Observable可以使用執行緒池併發的執行,同時flatmap還會將這n個Observable merge成一個Observable。你可以將其中的註釋開啟,看看執行緒的執行情況。

效能還不錯:

RxJavaWithFlatMap TPS: 3906

FlatMap — transform the items emitted by an Observable into Observables, then flatten the emissions from those into a single Observable

7 另一種解決方案

我們已經清楚了要並行執行提高吞吐率的解決辦法就是建立多個Observable並且併發執行。基於這種解決方案,我們還可以有其它的解決方案。

上一方案中利用flatmap建立多個Observable,針對我們的例子,我們何不直接建立多個Observable呢?

public static void testRxJavaWithParallel(int count) throws Exception {
    ExecutorService es = Executors.newFixedThreadPool(200, new ThreadFactoryBuilder().setNameFormat("SubscribeOn-%d").build());
    URL url = new URL("http://127.0.0.1:8999/");
    CountDownLatch finishedLatch = new CountDownLatch(count);
    long t = System.nanoTime();
    for (int k = 0; k < count; k++) {
        Observable.just(k).map(i -> {
            //System.out.println("A: " + Thread.currentThread().getName());
            try {
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                int responseCode = conn.getResponseCode();
                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    //response.append(inputLine);
                }
                in.close();
                return responseCode;
            } catch (Exception ex) {
                return -1;
            }
        }).subscribeOn(Schedulers.from(es)).observeOn(Schedulers.computation()).subscribe(statusCode -> {
        }, error -> {
        }, () -> {
            finishedLatch.countDown();
        });
    }
    finishedLatch.await();
    t = (System.nanoTime() - t) / 1000000; //ms
    System.out.println("RxJavaWithParallel TPS: " + count * 1000 / t);
    es.shutdownNow();
}

效能更好一點:

RxJavaWithParallel2 TPS: 4716。

這個例子沒有使用Schedulers.io()作為它的排程器,這是因為如果在大併發的情況下,可能會出現建立過多的執行緒導致資源不錯,所以我們限定使用200個執行緒。

8 總結

  • subscribeOn() 改變的Observable執行(operate)使用的排程器,多次呼叫無效。
  • observeOn() 改變Observable傳送notifications的排程器,會影響後續的操作,可以多次呼叫
  • 預設情況下, 操作鏈使用的執行緒是呼叫subscribe()的執行緒
  • Schedulers提供了多個排程器,可以並行執行多個Observable
  • 使用RxJava可以實現非同步程式設計,但是依然要小心執行緒阻塞。而且由於這種非同步的程式設計,除錯程式碼可能更加的困難

相關文章