[英]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 和 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生成的線程。
注意: subscribeOn和publishOn方法可用於獲得對線程的更細粒度控制。
現在, block
、 blockFirst
和blockLast
方法會改變操作的調度/執行方式嗎? 答案是否定的。
當您使用塊時,在內部,流被訂閱為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.