響應(yīng)式編程

響應(yīng)式編程

本文是對(duì)Reactor官方文檔Introduction to Reactive Programming部分內(nèi)容的翻譯,希望對(duì)理解響應(yīng)式編程的概念和原理有所幫助。

響應(yīng)式編程是一種異步編程范式,它關(guān)注數(shù)據(jù)流和變化的傳播。這意味著可以通過(guò)使用編程語(yǔ)言輕松地表示靜態(tài)(例如數(shù)組)和動(dòng)態(tài)(例如事件發(fā)射器)數(shù)據(jù)流。

Reactive Streams為基于JVM的響應(yīng)庫(kù)提供了規(guī)范,它定義了一組接口和交互規(guī)則。在Java 9中,這些接口已經(jīng)集成到java.util.concurrent.Flow類之下。

在面向?qū)ο缶幊陶Z(yǔ)言中,響應(yīng)式編程通常以觀察者模式的擴(kuò)展呈現(xiàn)。還可以將響應(yīng)式流模式和迭代器模式比較,一個(gè)主要的區(qū)別是,迭代器基于”拉“,而響應(yīng)式流基于”推“。

使用迭代器是一種命令式編程,由開(kāi)發(fā)者決定何時(shí)去訪問(wèn)序列中的next()元素。而在響應(yīng)式流中,與Iterable-Iterator對(duì)應(yīng)的是Publisher-Subscriber。當(dāng)新的可用元素出現(xiàn)時(shí),發(fā)布者通知訂閱者,這種”推“正是響應(yīng)的關(guān)鍵。此外,應(yīng)用于推入元素上的操作是聲明式的而不是命令式的:程序員要做的是表達(dá)計(jì)算的邏輯,而不是描述精準(zhǔn)的控制流程。

除了推送元素,響應(yīng)式編程還定義了良好的錯(cuò)誤處理和完成通知方式。發(fā)布者可以通過(guò)調(diào)用next()方法推送新的元素給訂閱者,也可以通過(guò)調(diào)用onError()方法發(fā)送一個(gè)錯(cuò)誤信號(hào)或者調(diào)用onComplete()發(fā)送一個(gè)完成信號(hào)。錯(cuò)誤信號(hào)和完成信號(hào)都會(huì)終止序列。

響應(yīng)式編程非常靈活,它支持沒(méi)有值、一個(gè)值或n個(gè)值的用例(包括無(wú)限序列,例如時(shí)鐘的連續(xù)滴答聲)。

但是讓我們首先考慮,為什么我們需要響應(yīng)式編程?

阻塞可能是浪費(fèi)的

現(xiàn)代的應(yīng)用需要滿足大量的用戶并發(fā)訪問(wèn),盡管硬件的能力依然在不斷提高,軟件的性能仍然是一個(gè)關(guān)鍵問(wèn)題。

通常有兩種方法可以提升程序的性能:

  • 并行化:使用更多的線程和更多的硬件資源。
  • 在如何使用現(xiàn)有資源方面尋求更高的效率。

通常,Java開(kāi)發(fā)者使用阻塞代碼編程。在出現(xiàn)性能瓶頸之前,這種做法沒(méi)有問(wèn)題,此時(shí)就需要引入額外的線程,來(lái)運(yùn)行相似的阻塞代碼。但是,這種資源利用率的擴(kuò)展可能很快引來(lái)爭(zhēng)用和并發(fā)問(wèn)題。

更糟糕的是,阻塞會(huì)浪費(fèi)資源。如果仔細(xì)觀察,只要程序涉及一些延遲(特別是IO,比如數(shù)據(jù)庫(kù)請(qǐng)求或網(wǎng)絡(luò)調(diào)用),資源就會(huì)被浪費(fèi),因?yàn)橐粋€(gè)(或多個(gè)線程)正處于空閑狀態(tài),等待數(shù)據(jù)。

所以,并行化方法并不是什么靈丹妙藥。為了提高硬件資源的利用率,響應(yīng)式編程是必要的。但是它也很復(fù)雜,因?yàn)槿菀自斐少Y源浪費(fèi)。

使用異步來(lái)解決?

上文提到的第二種方法是尋求更高的效率,可以解決資源浪費(fèi)問(wèn)題。通過(guò)編寫(xiě)異步非阻塞代碼,你可以將執(zhí)行切換到另一個(gè)使用相同底層資源的活動(dòng)任務(wù)上,在異步執(zhí)行完成后返回到當(dāng)前程序。

如何在JVM上編寫(xiě)異步代碼呢?Java提供了兩種異步編程模型:

  • Callbacks:異步方法沒(méi)有返回值,但是提供一個(gè)額外的回調(diào)參數(shù)(一個(gè)lambda或者匿名類對(duì)象),當(dāng)結(jié)果可用時(shí)調(diào)用該參數(shù)。
  • Futures:異步方法立即返回一個(gè)Future<T>對(duì)象。異步線程計(jì)算一個(gè)T值,Future對(duì)象封裝對(duì)它的訪問(wèn)。該值不是立即可用的,但可以輪詢Future對(duì)象,直到該值可用為止。

這些技術(shù)足夠好嗎?并不是每個(gè)場(chǎng)景都適用,而且兩種方式都有限制。

回調(diào)是很難組合在一起的,很快就會(huì)導(dǎo)致難以閱讀和維護(hù)的代碼(稱為”回調(diào)地獄”)。

Futures比回調(diào)稍微好一點(diǎn),但是在組合方面依然做的不夠好,即使Java 8中引入了CompletableFuture。把多個(gè)Future編排到一起雖然可行,但是并不容易。而且,Future還有另外的問(wèn)題:通過(guò)調(diào)用get()方法,很容易讓Future對(duì)象進(jìn)入到另一種阻塞情景;它們不支持延時(shí)計(jì)算;不支持多值和高級(jí)錯(cuò)誤處理。

從命令式編程到響應(yīng)式編程

響應(yīng)式編程庫(kù)Reactor旨在解決JVM上這些”經(jīng)典“的異步編程方式的缺點(diǎn),同時(shí)關(guān)注幾個(gè)額外的方面:

  • 可組合性和可讀性;
  • 使用豐富的操作符詞匯操作數(shù)據(jù)流;
  • 在訂閱數(shù)據(jù)流之前什么也不會(huì)發(fā)生(延時(shí)計(jì)算);
  • 背壓(backpressure)或消費(fèi)者向生產(chǎn)者發(fā)送發(fā)射速率過(guò)快的信號(hào)的能力;
  • 與并發(fā)無(wú)關(guān)的高級(jí)抽象;

可組合性和可讀性

可組合性指的是編排、協(xié)調(diào)多個(gè)異步任務(wù)的能力。包括使用先前任務(wù)的結(jié)果為后續(xù)任務(wù)提供輸入,或者以fork-join格式執(zhí)行多個(gè)任務(wù)以及在更高級(jí)別的系統(tǒng)中將異步任務(wù)作為獨(dú)立組件重用。

編排任務(wù)的能力與代碼的可讀性和可維護(hù)性是緊密相關(guān)的。隨著異步處理的數(shù)量和復(fù)雜性的增加,編寫(xiě)和閱讀代碼變得越來(lái)越困難。正如我們所看到的,回調(diào)模式很簡(jiǎn)單,但是它的一個(gè)主要缺點(diǎn)是,對(duì)于復(fù)雜的任務(wù),你需要從回調(diào)中執(zhí)行回調(diào),回調(diào)本身嵌套在另一個(gè)回調(diào)中,等等。這種混亂被稱為回調(diào)地獄。可以想象,這樣的代碼很難回頭進(jìn)行推理。

裝配線類比

你可以將在響應(yīng)式應(yīng)用中處理數(shù)據(jù)想象成(數(shù)據(jù))在裝配線上移動(dòng)。響應(yīng)式編程既是傳送帶,又是工作站。原材料從一個(gè)數(shù)據(jù)源(最初的Publisher)中傾瀉而出,最終成為準(zhǔn)備推送給消費(fèi)者(Subscriber)的成品。

原材料可以經(jīng)過(guò)各種轉(zhuǎn)換和中間步驟,或者成為將中間部件組裝在一起的大型裝配線的一部分。如果在某一點(diǎn)發(fā)生了小故障或者阻塞,受影響的工作站可以向上游發(fā)信號(hào)限制原材料的流動(dòng)。

操作符

操作符就是裝配線中的工作站。每一個(gè)操作符都向發(fā)布者(Publisher)添加新的行為,并將上一步中的發(fā)布者包裝到一個(gè)新的實(shí)例中。整個(gè)鏈條就是這樣連接在一起的,數(shù)據(jù)源自第一個(gè)發(fā)布者(Publisher),并沿著鏈條向下移動(dòng),在每個(gè)鏈接處被轉(zhuǎn)換。最終,一個(gè)訂閱者(Subscriber)結(jié)束這個(gè)流程。

訂閱前什么也不會(huì)發(fā)生

在Reactor中,當(dāng)你編寫(xiě)一個(gè)發(fā)布者(Publiser)鏈時(shí),默認(rèn)情況下不會(huì)開(kāi)始向其中注入數(shù)據(jù)。相反,你創(chuàng)建的是一個(gè)異步處理的抽象描述(這有助于重用性和組合)。

通過(guò)訂閱操作(Subscribe()),你將發(fā)布者綁定到一個(gè)訂閱者,從而觸發(fā)整個(gè)鏈中的數(shù)據(jù)流。這是通過(guò)訂閱者(Subscriber)發(fā)出一個(gè)請(qǐng)求(request())信號(hào)在內(nèi)部實(shí)現(xiàn)的,該信號(hào)向上游傳播,一直到源頭發(fā)布者(Publisher)。

背壓

向上游傳播信號(hào)也被用來(lái)實(shí)現(xiàn)背壓。在裝配線類比中,我們將其描述為當(dāng)工作站處理速度比上游工作站慢時(shí),沿裝配線向上游發(fā)送的反饋信號(hào)。

響應(yīng)式流規(guī)范中定義的真正的機(jī)制與裝配線類比非常相似:訂閱者可以在無(wú)限制的模式下工作,并讓數(shù)據(jù)源以最快的速度推送數(shù)據(jù);或者它可以用request()機(jī)制向數(shù)據(jù)源發(fā)出信號(hào),表明自己最多可以處理n個(gè)數(shù)據(jù)。

中間操作符還可以在傳輸過(guò)程中更改請(qǐng)求。假設(shè)有一個(gè)buffer操作符以10個(gè)元素為一個(gè)批次對(duì)元素進(jìn)行分組。如果訂閱者請(qǐng)求1個(gè)緩沖區(qū),數(shù)據(jù)源就要生成10個(gè)元素。一些操作符還實(shí)現(xiàn)預(yù)?。?code>prefetching)策略,這避免了每次請(qǐng)求一個(gè)元素的往返開(kāi)銷,如果在被請(qǐng)求之前生成元素的成本不是太高,那么這樣做是有益的。

預(yù)取策略將推模型轉(zhuǎn)換為推-拉混合模型,在這種混合模型中,下游可以從上游拉出n個(gè)元素(如果它們已經(jīng)可用)。但是,如果元素還沒(méi)有準(zhǔn)備好,它們會(huì)在生產(chǎn)好之后由上游推給下游。

熱響應(yīng)式序列和冷響應(yīng)式序列

在響應(yīng)式編程庫(kù)的Rx家族中,我們可以區(qū)分兩大類響應(yīng)式序列:熱響應(yīng)式序列和冷響應(yīng)式序列 。這種區(qū)別主要與響應(yīng)式流如何對(duì)訂閱者做出響應(yīng)有關(guān):

  • 冷響應(yīng)式序列對(duì)每個(gè)訂閱者(包括數(shù)據(jù)源)會(huì)重新啟動(dòng)一個(gè)全新的響應(yīng)序列。
  • 熱響應(yīng)式序列對(duì)每個(gè)訂閱者不會(huì)重零開(kāi)始。相反,后來(lái)的訂閱者只能接收到他們訂閱后發(fā)出的數(shù)據(jù)。但是,請(qǐng)注意,一些熱響應(yīng)式序列可以緩存或回放全部或部分?jǐn)?shù)據(jù)發(fā)布?xì)v史。從一般的角度來(lái)看,熱序列甚至可以在沒(méi)有訂閱者在監(jiān)聽(tīng)時(shí)發(fā)射數(shù)據(jù)(“訂閱前什么都不會(huì)發(fā)生”規(guī)則的一個(gè)例外)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容