簡體   English   中英

Spring Reactor 的線程 model 是什么,and.subscribe() 好像沒有利用多線程?

[英]What is the threading model of Spring Reactor, and .subscribe() seems to not utilise more than a single thread?

響應式編程概念仍然很新,所以請多多包涵。 我對反應鏈進行了更多測試,下面是以下代碼:

                Flux
                        .range(0, 10000)
                        .doOnNext(i -> {
                            System.out.println("start " + i+ " Thread: " + Thread.currentThread().getName());
                            try {
                                Thread.sleep(2000);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        })
                        .flatMap(i -> {
                            System.out.println("end"+ i + " Thread: " + Thread.currentThread().getName());
                            return Mono.just(i);
                        })
                        .doOnError(err -> {
                            throw new RuntimeException(err.getMessage());
                        })
                        .subscribe();
System.out.println("Thread: " + Thread.currentThread().getName() + " Hello world");

我在這里的主要困惑是 output:

start 0 Thread: main
end0 Thread: main
start 1 Thread: main
end1 Thread: main
start 2 Thread: main
end2 Thread: main
start 3 Thread: main
end3 Thread: main
start 4 Thread: main
end4 Thread: main
Thread: main Hello world

為什么沒有生成/使用默認線程池來更有效地處理每個 integer? 另外,根據我對訂閱發布者的使用的理解:

Subscribe is an asynchronous process. It means that when you call subscribe, it launch processing in the background, then return immediately. 

然而,我的觀察似乎與我在這里觀察到阻塞行為的地方不同,因為“Hello World”的打印必須首先等待 Flux Reactive 鏈的處理首先完成,因為該鏈正在使用並阻塞(?)主線程。

取消注釋subscribeOn()有一種不同的、“正確”的行為:

Flux
                            .range(0, 10000)
                            .doOnNext(i -> {
                                System.out.println("start " + i+ " Thread: " + Thread.currentThread().getName());
                                try {
                                    Thread.sleep(2000);
                                } catch (InterruptedException e) {
                                    throw new RuntimeException(e);
                                }
                            })
                            .flatMap(i -> {
                                System.out.println("end"+ i + " Thread: " + Thread.currentThread().getName());
                                return Mono.just(i);
                            })
                            .doOnError(err -> {
                                throw new RuntimeException(err.getMessage());
                            })
                            .subscribeOn(Schedulers.boundedElastic())
                            .subscribe();
    System.out.println("Thread: " + Thread.currentThread().getName() + " Hello world");

Thread: main Hello world
start 0 Thread: boundedElastic-1
end0 Thread: boundedElastic-1
start 1 Thread: boundedElastic-1
end1 Thread: boundedElastic-1
start 2 Thread: boundedElastic-1

我對此的理解是因為我們現在指定了反應鏈必須用於其處理的 threadPool,這樣主線程就可以自由地表現得暢通無阻,首先異步打印“Hello World”。

同樣,如果我們現在將.subscribe()替換為.blockLast()

            Flux
                    .range(0, 5)
                    .doOnNext(i -> {
                        System.out.println("start " + i+ " Thread: " + Thread.currentThread().getName());
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    })
                    .flatMap(i -> {
//                        if (i == 100) return Mono.error(new RuntimeException("Died cuz i = 100"));
                        System.out.println("end"+ i + " Thread: " + Thread.currentThread().getName());
                        return Mono.just(i);
                    })
                    .doOnError(err -> {
                        throw new RuntimeException(err.getMessage());
                    })
                    .subscribeOn(Schedulers.boundedElastic())
                    .blockLast();
            System.out.println("Thread: " + Thread.currentThread().getName() + " Hello world");

由此產生的行為是預期的,更改為阻塞而不是異步,如果我的理解是正確的,那是因為即使我們指定了一個不同的線程池(不是主線程)用於反應鏈的處理,主線程是調用者線程,仍然被阻塞,直到反應鏈在釋放主線程之前返回成功或錯誤信號。

到目前為止我的理解合理嗎? 我對 Project Reactor 的默認線程行為的理解哪里出錯了?

我建議您閱讀官方文檔中與線程相關的部分 它應該讓您對發生的事情有一個很好的理解。 我會盡量總結一下。

為什么 Flux 在調用線程上處理?

Flux 和 Mono 對象 model 一個可暫停、可恢復的操作鏈。 這意味着引擎可以“停放”操作,然后在可用線程上安排它們的執行。

現在,當您在Publisher上調用subscribe時,您就開始了操作鏈。 它的第一個動作是在調用線程上啟動的,正如文檔中引用的那樣:

除非指定,否則最頂層的運算符(源)本身在調用 subscribe() 的線程上運行。

如果您的流程足夠簡單,則很有可能整個通量/單聲道將在同一線程上處理。

這是否意味着它被同步調用/在程序前台調用?

這可能會給人一種同步處理的錯覺,但事實並非如此。

我們已經可以在您的第一個示例中看到它。 您創建了一個包含一千個值的范圍,但在Thread: main Hello world消息之前只打印了 4 個值。 它表明處理已經開始,但是已經“暫停”讓你的主程序繼續。

我們還可以在以下示例中更清楚地看到這一點:

// Assemble the flow of operations
Flux flow = Flux.range(0, 5)
        .flatMap(i -> Mono
                .just("flatMap on [" + Thread.currentThread().getName() + "] -> " + i)
                .delayElement(Duration.ofMillis(50)));

// Launch processing
Disposable execStatus = flow.subscribe(System.out::println);

System.out.println("SUBSCRIPTION DONE");

// Prevent program to stop before the flow is over.
do {
    System.out.println("Waiting for the flow to end...");
    Thread.sleep(50);
} while (!execStatus.isDisposed());

System.out.println("FLUX PROCESSED");

這個程序打印:

SUBSCRIPTION DONE
Waiting for the flow to end...
flatMap on [main] -> 0
flatMap on [main] -> 1
flatMap on [main] -> 3
Waiting for the flow to end...
flatMap on [main] -> 4
flatMap on [main] -> 2
FLUX PROCESSED

如果我們看一下,我們可以看到來自主程序的消息與主程序交織在一起,所以即使流在主線程上執行,它也是在后台執行的 n.netheless。

這證明了subscribe(Consumer) apidoc 所說的內容:

請記住,由於序列可以是異步的,這將立即將控制返回給調用線程。

為什么不涉及其他線程?

現在,為什么沒有使用其他線程? 嗯,這是一個復雜的問題。 在這種情況下,我會說引擎決定不需要其他線程來以良好的性能執行管道。 線程之間的切換是有代價的,所以如果可以避免的話,我覺得Reactor就避免了。

可以涉及其他線程嗎?

文件指出:

一些 Operator 默認使用 Schedulers 中的特定調度程序

這意味着根據管道的不同,它的任務可能會在調度程序提供的線程上分派。

事實上, flatMap應該這樣做。 如果我們只稍微修改示例,我們將看到操作被分派到並行調度程序 我們所要做的就是限制並發(是的,我知道,這不是很直觀)。 默認情況下, flatMap使用並發因子 256。這意味着它可以同時啟動 256 個操作(粗略解釋)。 讓我們將其限制為 2:

Flux flow = Flux.range(0, 5)
        .flatMap(i -> Mono
                .just("flatMap on [" + Thread.currentThread().getName() + "] -> " + i)
                .delayElement(Duration.ofMillis(50)),
                2);

現在,程序打印:

SUBSCRIPTION DONE
Waiting for the flow to end...
flatMap on [main] -> 0
Waiting for the flow to end...
flatMap on [main] -> 1
Waiting for the flow to end...
flatMap on [parallel-1] -> 2
flatMap on [parallel-2] -> 3
Waiting for the flow to end...
flatMap on [parallel-3] -> 4
FLUX PROCESSED

我們看到操作 2、3 和 4 發生在名為parallel-x的線程上。 這些是由Schedulers.parallel生成的線程。

注意: subscribeOnpublishOn方法可用於獲得對線程的更細粒度控制。

關於屏蔽

現在, blockblockFirstblockLast方法會改變操作的調度/執行方式嗎? 答案是否定的。

當您使用塊時,在內部,流被訂閱為subscribe()調用。 然而,一旦流被觸發,而不是返回,Reactor 在內部使用調用線程循環並等待通量完成,就像我在上面的第一個示例中所做的那樣(但他們以一種非常聰明的方式做到了)。

我們可以試試。 使用受約束的 flatMap,如果我們使用塊而不是手動循環打印什么?

該程序:

Flux.range(0, 5)
        .flatMap(i -> Mono
                .just("flatMap on [" + Thread.currentThread().getName() + "] -> " + i)
                .delayElement(Duration.ofMillis(50)),
                2)
        .doOnNext(System.out::println)
        .blockLast();

System.out.println("FLUX PROCESSED");

印刷:

flatMap on [main] -> 0
flatMap on [main] -> 1
flatMap on [parallel-1] -> 2
flatMap on [parallel-2] -> 3
flatMap on [parallel-3] -> 4
FLUX PROCESSED

我們看到,如前所述,flux 同時使用主線程和並行線程來處理其元素。 但是這一次,主程序被“暫停”,直到流程結束。 Block 阻止我們的程序繼續運行,直到 Flux 完成。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM