響應(yīng)式編程

響應(yīng)式編程

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

使用異步來解決?

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

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

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

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

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

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

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

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

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

可組合性和可讀性

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

編排任務(wù)的能力與代碼的可讀性和可維護(hù)性是緊密相關(guān)的。隨著異步處理的數(shù)量和復(fù)雜性的增加,編寫和閱讀代碼變得越來越困難。正如我們所看到的,回調(diào)模式很簡單,但是它的一個(gè)主要缺點(diǎn)是,對于復(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)過各種轉(zhuǎn)換和中間步驟,或者成為將中間部件組裝在一起的大型裝配線的一部分。如果在某一點(diǎn)發(fā)生了小故障或者阻塞,受影響的工作站可以向上游發(fā)信號限制原材料的流動(dòng)。

操作符

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

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

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

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

背壓

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

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

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

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

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

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

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

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

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