2021-02-18 分類(lèi): 網(wǎng)站建設(shè)
近期嘗試在搬磚專(zhuān)用語(yǔ)言 Java 上實(shí)現(xiàn)異步,起因和過(guò)程就不再詳述了,總而言之,心中一萬(wàn)頭草泥馬奔過(guò)。但這個(gè)過(guò)程也沒(méi)有白白浪費(fèi),趁機(jī)回顧了一下各種異步編程的實(shí)現(xiàn)。
這篇文章會(huì)涉及到回調(diào)、Promise、反應(yīng)式、async/await、用戶(hù)態(tài)線(xiàn)程等異步編程的實(shí)現(xiàn)方案。如果你熟悉它們中的一兩種,那應(yīng)該也能很快理解其他幾個(gè)。
為什么需要異步?
操作系統(tǒng)可以看作是個(gè)虛擬機(jī)(VM),進(jìn)程生活在操作系統(tǒng)創(chuàng)造的虛擬世界里。進(jìn)程不用知道到底有多少 core 多少內(nèi)存,只要進(jìn)程不要索取的太過(guò)分,操作系統(tǒng)就假裝有無(wú)限多的資源可用。
基于這個(gè)思想,線(xiàn)程(Thread)的個(gè)數(shù)并不受硬件限制:你的程序可以只有一個(gè)線(xiàn)程、也可以有成百上千個(gè)。操作系統(tǒng)會(huì)默默做好調(diào)度,讓諸多線(xiàn)程共享有限的 CPU 時(shí)間片。這個(gè)調(diào)度的過(guò)程對(duì)線(xiàn)程是完全透明的。
那么,操作系統(tǒng)是怎樣做到在線(xiàn)程無(wú)感知的情況下調(diào)度呢?答案是上下文切換(Context Switch),簡(jiǎn)單來(lái)說(shuō),操作系統(tǒng)利用軟中斷機(jī)制,把程序從任意位置打斷,然后保存當(dāng)前所有寄存器——包括最重要的指令寄存器 PC 和棧頂指針 SP,還有一些線(xiàn)程控制信息(TCB),整個(gè)過(guò)程會(huì)產(chǎn)生數(shù)個(gè)微秒的 overhead。
然而作為一位合格的程序員,你一定也聽(tīng)說(shuō)過(guò),線(xiàn)程是昂貴的:
這兩個(gè)原因驅(qū)使我們盡可能避免創(chuàng)建太多的線(xiàn)程,而異步編程的目的就是消除 IO wait 阻塞——絕大多數(shù)時(shí)候,這是我們創(chuàng)建一堆線(xiàn)程、甚至引入線(xiàn)程池的罪魁禍?zhǔn)住?/p>
Continuation
回調(diào)函數(shù)知道的人很多,但了解 Continuation 的人不多。Continuation 有時(shí)被晦澀地翻譯成 “計(jì)算續(xù)體”,咱們還是直接用單詞好了。
把一個(gè)計(jì)算過(guò)程在中間打斷,剩下的部分用一個(gè)對(duì)象表示,這就是 Continuation。操作系統(tǒng)暫停一個(gè)線(xiàn)程時(shí)保存的那些現(xiàn)場(chǎng)數(shù)據(jù),也可以看作一個(gè) Continuation。有了它,我們就能在這個(gè)點(diǎn)接著剛剛的斷點(diǎn)繼續(xù)執(zhí)行。
打斷一個(gè)計(jì)算過(guò)程聽(tīng)起來(lái)很厲害吧!實(shí)際上它每時(shí)每刻都在發(fā)生——假設(shè)函數(shù) f() 中間調(diào)用了 g(),那 g() 運(yùn)行完成時(shí),要返回到 f() 剛剛調(diào)用 g() 的地方接著執(zhí)行。這個(gè)過(guò)程再自然不過(guò)了,以至于所有編程語(yǔ)言(匯編除外)都把它掩藏起來(lái),讓你在編程中感覺(jué)不到調(diào)用棧的存在。
操作系統(tǒng)用昂貴的軟中斷機(jī)制實(shí)現(xiàn)了棧的保存和恢復(fù)。那有沒(méi)有別的方式實(shí)現(xiàn) Continuation 呢?最樸素的想法就是,把所有用得到的信息包成一個(gè)函數(shù)對(duì)象,在調(diào)用 g() 的時(shí)候一起傳進(jìn)去,并約定:一旦 g() 完成,就拿著結(jié)果去調(diào)用這個(gè) Continuation。
這種編程模式被稱(chēng)為 Continuation-passing style(CPS):
再拿 Wikipedia 上的定義鞏固一下:
A function written in continuation-passing style takes an extra argument: an explicit “continuation”, i.e. a function of one argument. When the CPS function has computed its result value, it “returns” it by calling the continuation function with this value as the argument.CPS 風(fēng)格的函數(shù)帶一個(gè)額外的參數(shù):一個(gè)顯式的 Continuation,具體來(lái)說(shuō)就是個(gè)僅有一個(gè)參數(shù)的函數(shù)。當(dāng) CPS 函數(shù)計(jì)算完返回值時(shí),它 “返回” 的方式就是拿著返回值調(diào)用那個(gè) Continuation。你應(yīng)該已經(jīng)發(fā)現(xiàn)了,這也就是回調(diào)函數(shù),我只是換了個(gè)名字而已。
異步的樸素實(shí)現(xiàn):Callback
光有回調(diào)函數(shù)其實(shí)并沒(méi)有卵用。對(duì)于純粹的計(jì)算工作,Call Stack 就很好,為何要費(fèi)時(shí)費(fèi)力用回調(diào)來(lái)做 Continuation 呢?你說(shuō)的對(duì),但僅限于沒(méi)有 IO 的情況。我們知道 IO 通常要比 CPU 慢上好幾個(gè)數(shù)量級(jí),在 BIO 中,線(xiàn)程發(fā)起 IO 之后只能暫停,然后等待 IO 完成再由操作系統(tǒng)喚醒。
var input = recv_from_socket() // Block at syscall recv()var result = calculator.calculate(input)send_to_socket(result) // Block at syscall send()
而異步 IO 中,進(jìn)程發(fā)起 IO 操作時(shí)也會(huì)一并輸入回調(diào)(也就是 Continuation),這大大解放了生產(chǎn)力——現(xiàn)場(chǎng)無(wú)需等待,可以立即返回去做其他事情。一旦 IO 成功后,AIO 的 Event Loop 會(huì)調(diào)用剛剛設(shè)置的回調(diào)函數(shù),把剩下的工作完成。這種模式有時(shí)也被稱(chēng)為 Fire and Forget。
recv_from_socket((input) -> { var result = calculator.calculate(input) send_to_socket(result) // ignore result})
就這么簡(jiǎn)單,通過(guò)我們自己實(shí)現(xiàn)的 Continuation,線(xiàn)程不再受 IO 阻塞,可以自由自在地跑滿(mǎn) CPU。
一顆語(yǔ)法糖:Promise
回調(diào)函數(shù)哪里都好,就是不大好用,以及太丑了。
第一個(gè)問(wèn)題是可讀性大大下降,由于我們繞開(kāi)操作系統(tǒng)自制 Continuation,所有函數(shù)調(diào)用都要傳入一個(gè) lambda 表達(dá)式,你的代碼看起來(lái)就像要起飛一樣,縮進(jìn)止不住地往右挪(the “Callback Hell”)。
第二個(gè)問(wèn)題是各種細(xì)節(jié)處理起來(lái)很麻煩,比如,考慮下異常處理,看來(lái)傳一個(gè) Continuation 還不夠,最好再傳個(gè)異常處理的 callback。
Promise 是對(duì)異步調(diào)用結(jié)果的一個(gè)封裝,在 Java 中它叫作 CompletableFuture (JDK8) 或者 ListenableFuture (Guava)。Promise 有兩層含義:
第一層含義是:我現(xiàn)在還不是真正的結(jié)果,但是承諾以后會(huì)拿到這個(gè)結(jié)果。這很容易理解,異步的任務(wù)遲早會(huì)完成,調(diào)用者如果比較蠢萌,他也可以用 Promise.get() 強(qiáng)行要拿到結(jié)果,順便阻塞了當(dāng)前線(xiàn)程,異步變成了同步。
第二層含義是:如果你(調(diào)用者)有什么吩咐,就告訴我好了。這就有趣了,換句話(huà)說(shuō),回調(diào)函數(shù)不再是傳給 g(),而是 g() 返回的 Promise,比如之前那段代碼,我們用 Promise 來(lái)書(shū)寫(xiě),看起來(lái)順眼了不少。
var promise_input = recv_from_socket()promise_input.then((input) -> { var result = calculator.calculate(input) send_to_socket(result) // ignore result})
Promise 改善了 Callback 的可讀性,也讓異常處理稍稍?xún)?yōu)雅了些,但終究是顆語(yǔ)法糖。
反應(yīng)式編程
反應(yīng)式(Reactive)最早源于函數(shù)式編程中的一種模式,隨著微軟發(fā)起 ReactiveX 項(xiàng)目并一步步壯大,被移植到各種語(yǔ)言和平臺(tái)上。Reactive 最初在 GUI 編程中有廣泛的應(yīng)用,由于異步調(diào)用的高性能,很快也在服務(wù)器后端領(lǐng)域遍地開(kāi)花。
Reactive 可以看作是對(duì) Promise 的極大增強(qiáng),相比 Promise,反應(yīng)式引入了流(Flow)的概念。ReactiveX 中的事件流從一個(gè) Observable 對(duì)象流出,這個(gè)對(duì)象可以是一個(gè)按鈕,也可以是 Restful API,總之,它能被外界觸發(fā)。與 Promise 不同的是,事件可能被觸發(fā)多次,所以處理代碼也會(huì)被多次調(diào)用。
一旦允許調(diào)用多次,從數(shù)據(jù)流動(dòng)的角度看,事實(shí)上模型已經(jīng)是 Push 而非 Pull。那么問(wèn)題來(lái)了,如果調(diào)用頻率非常高,以至于我們處理速度跟不上了怎么辦?所以 RX 框架又引入了 Backpressure 機(jī)制來(lái)進(jìn)行流控,最簡(jiǎn)單的流控方式就是:一旦 buffer 滿(mǎn),就丟棄掉之后的事件。
ReactiveX 框架的另一個(gè)優(yōu)點(diǎn)是內(nèi)置了很多好用的算子,比如:merge(Flow 合并),debounce(開(kāi)關(guān)除顫)等等,方便了業(yè)務(wù)開(kāi)發(fā)。下面是一個(gè) RxJava 的例子:
CPS 變換:Coroutine 與 async/await
無(wú)論是反應(yīng)式還是 Promise,說(shuō)到底仍然沒(méi)有擺脫手工構(gòu)造 Continuation:開(kāi)發(fā)者要把業(yè)務(wù)邏輯寫(xiě)成回調(diào)函數(shù)。對(duì)于線(xiàn)性的邏輯基本可以應(yīng)付自如,但是如果邏輯復(fù)雜一點(diǎn)呢?(比如,考慮下包含循環(huán)的情況)
有些語(yǔ)言例如 C#,JavaScript 和 Python 提供了 async/await 關(guān)鍵字。與 Reactive 一樣,這同樣出自微軟 C# 語(yǔ)言。在這些語(yǔ)言中,你會(huì)感到前所未有的爽感:異步編程終于擺脫了回調(diào)函數(shù)!唯一要做的只是在異步函數(shù)調(diào)用時(shí)加上 await,編譯器就會(huì)自動(dòng)把它轉(zhuǎn)化為協(xié)程(Coroutine),而非昂貴的線(xiàn)程。
魔法的背后是 CPS 變換,CPS 變換把普通函數(shù)轉(zhuǎn)換成一個(gè) CPS 的函數(shù),即 Continuation 也能作為一個(gè)調(diào)用參數(shù)。函數(shù)不僅能從頭運(yùn)行,還能根據(jù) Continuation 的指示繼續(xù)某個(gè)點(diǎn)(比如調(diào)用 IO 的地方)運(yùn)行。
例子可以參見(jiàn)我的下一篇文章。由于代碼太長(zhǎng),就不貼在這兒了。
可以看到,函數(shù)已經(jīng)不再是一個(gè)函數(shù)了,而是變成一個(gè)狀態(tài)機(jī)。每次 call 它、或者它 call 其他異步函數(shù)時(shí),狀態(tài)機(jī)都會(huì)做一些計(jì)算和狀態(tài)輪轉(zhuǎn)。說(shuō)好的 Continuation 在哪呢?就是對(duì)象自己(this)啊。
CPS 變換實(shí)現(xiàn)非常復(fù)雜,尤其是考慮到 try-catch 之后。但是沒(méi)關(guān)系,復(fù)雜性都在編譯器里,用戶(hù)只要學(xué)兩個(gè)關(guān)鍵詞即可。這個(gè)特性非常優(yōu)雅,比 Java 那個(gè)廢柴的 CompletableFuture 不知道高到哪去了。(更新:也沒(méi)有那么廢柴啦)
JVM 上也有一個(gè)實(shí)現(xiàn):electronicarts/ea-async,原理和 C# 的 async/await 類(lèi)似,在編譯期修改 Bytecode 實(shí)現(xiàn) CPS 變換。終極方案:用戶(hù)態(tài)線(xiàn)程
有了 async/await,代碼已經(jīng)簡(jiǎn)潔很多了,基本上和同步代碼無(wú)異。是否有可能讓異步代碼和同步代碼完全一樣呢?聽(tīng)起來(lái)就像免費(fèi)午餐,但是的確可以做到!
用戶(hù)態(tài)線(xiàn)程的代表是 Golang。JVM 上也有些實(shí)現(xiàn),比如 Quasar,不過(guò)因?yàn)?JDBC、Spring 這些周邊生態(tài)(它們占據(jù)了大部分 IO 操作)的缺失基本沒(méi)有什么用。
用戶(hù)態(tài)線(xiàn)程是把操作系統(tǒng)提供的線(xiàn)程機(jī)制完全拋棄,換句話(huà)說(shuō),不去用這個(gè) VM 的虛擬化機(jī)制。比如硬件有 8 個(gè)核心,那就創(chuàng)建 8 個(gè)系統(tǒng)線(xiàn)程,然后把 N 個(gè)用戶(hù)線(xiàn)程調(diào)度到這 8 個(gè)系統(tǒng)線(xiàn)程上跑。N 個(gè)用戶(hù)線(xiàn)程的調(diào)度在用戶(hù)進(jìn)程里實(shí)現(xiàn),由于一切都在進(jìn)程內(nèi)部,切換代價(jià)要遠(yuǎn)遠(yuǎn)小于操作系統(tǒng) Context Switch。
另一方面,所有可能阻塞系統(tǒng)級(jí)線(xiàn)程的事情,例如 sleep()、recv() 等,用戶(hù)態(tài)線(xiàn)程一定不能碰,否則它一旦阻塞住也就帶著那 8 個(gè)系統(tǒng)線(xiàn)程中的一個(gè)阻塞了。Go Runtime 接管了所有這樣的系統(tǒng)調(diào)用,并用一個(gè)統(tǒng)一的 Event loop 來(lái)輪詢(xún)和分發(fā)。
另外,由于用戶(hù)態(tài)線(xiàn)程很輕量,我們完全沒(méi)必要再用線(xiàn)程池,如果需要開(kāi)線(xiàn)程就直接創(chuàng)建。比如 Java 中的 WebServer 幾乎一定有個(gè)線(xiàn)程池,而 Go 可以給每個(gè)請(qǐng)求開(kāi)辟一個(gè) goroutine 去處理。并發(fā)編程從未如此美好!
總結(jié)
以上方案中,Promise、Reactive 本質(zhì)上還是回調(diào)函數(shù),只是框架的存在一定程度上降低了開(kāi)發(fā)者的心智負(fù)擔(dān)。而 async/await 和用戶(hù)態(tài)線(xiàn)程的解決方案要優(yōu)雅和徹底的多,前者通過(guò)編譯期的 CPS 變換幫用戶(hù)創(chuàng)造出 CPS 式的函數(shù)調(diào)用;后者則繞開(kāi)操作系統(tǒng)、重新實(shí)現(xiàn)一套線(xiàn)程機(jī)制,一切調(diào)度工作由 Runtime 接管。
不知道是不是因?yàn)闅v史包袱太重,Java 語(yǔ)言本身提供的異步編程支持弱得可憐,即便是 CompletableFuture 還是在 Java 8 才引入,其后果就是很多庫(kù)都沒(méi)有異步的支持。雖然 Quasar 在沒(méi)有語(yǔ)言級(jí)支持的情況下引入了 CPS 變換,但是由于缺少周邊生態(tài)的支持,實(shí)際很難用在項(xiàng)目中。
網(wǎng)站名稱(chēng):圖示Java異步編程,清晰易懂,收藏了
當(dāng)前路徑:http://m.newbst.com/news47/101597.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站建設(shè)、網(wǎng)站策劃、域名注冊(cè)、動(dòng)態(tài)網(wǎng)站、網(wǎng)站制作、移動(dòng)網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容