<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          異步編程的幾種方式,你知道幾種?

          共 6334字,需瀏覽 13分鐘

           ·

          2021-05-28 10:37



          來源:ericfu.me/several-ways-to-aync/

          • 為什么需要異步?
          • Continuation
          • 異步的樸素實現(xiàn):Callback
          • 一顆語法糖:Promise
          • 反應(yīng)式編程
          • CPS 變換:Coroutine 與 async/await
          • 終極方案:用戶態(tài)線程
          • 總結(jié)

          近期嘗試在搬磚專用語言 Java 上實現(xiàn)異步,起因和過程就不再詳述了,總而言之,心中一萬頭草泥馬奔過。但這個過程也沒有白白浪費,趁機(jī)回顧了一下各種異步編程的實現(xiàn)。

          這篇文章會涉及到回調(diào)、Promise、反應(yīng)式、async/await、用戶態(tài)線程等異步編程的實現(xiàn)方案。如果你熟悉它們中的一兩種,那應(yīng)該也能很快理解其他幾個。

          為什么需要異步?

          操作系統(tǒng)可以看作是個虛擬機(jī)(VM),進(jìn)程生活在操作系統(tǒng)創(chuàng)造的虛擬世界里。進(jìn)程不用知道到底有多少 core 多少內(nèi)存,只要進(jìn)程不要索取的太過分,操作系統(tǒng)就假裝有無限多的資源可用。

          基于這個思想,線程(Thread)的個數(shù)并不受硬件限制:你的程序可以只有一個線程、也可以有成百上千個。操作系統(tǒng)會默默做好調(diào)度,讓諸多線程共享有限的 CPU 時間片。這個調(diào)度的過程對線程是完全透明 的。

          那么,操作系統(tǒng)是怎樣做到在線程無感知的情況下調(diào)度呢?答案是上下文切換(Context Switch) ,簡單來說,操作系統(tǒng)利用軟中斷機(jī)制,把程序從任意位置打斷,然后保存當(dāng)前所有寄存器——包括最重要的指令寄存器 PC 和棧頂指針 SP,還有一些線程控制信息(TCB),整個過程會產(chǎn)生數(shù)個微秒的 overhead。

          然而作為一位合格的程序員,你一定也聽說過,線程是昂貴的:

          • 線程的上下文切換有不少的代價,占用寶貴的 CPU 時間;
          • 每個線程都會占用一些(至少 1 頁)內(nèi)存。

          這兩個原因驅(qū)使我們盡可能避免創(chuàng)建太多的線程 ,而異步編程的目的就是消除 IO wait 阻塞——絕大多數(shù)時候,這是我們創(chuàng)建一堆線程、甚至引入線程池的罪魁禍?zhǔn)住?/p>

          Continuation

          回調(diào)函數(shù)知道的人很多,但了解 Continuation 的人不多。Continuation 有時被晦澀地翻譯成“計算續(xù)體”,咱們還是直接用單詞好了。

          把一個計算過程在中間打斷,剩下的部分用一個對象表示,這就是 Continuation 。操作系統(tǒng)暫停一個線程時保存的那些現(xiàn)場數(shù)據(jù),也可以看作一個 Continuation。有了它,我們就能在這個點接著剛剛的斷點繼續(xù)執(zhí)行。

          打斷一個計算過程聽起來很厲害吧!實際上它每時每刻都在發(fā)生——假設(shè)函數(shù) f() 中間調(diào)用了 g(),那 g() 運行完成時,要返回到 f() 剛剛調(diào)用 g() 的地方接著執(zhí)行。這個過程再自然不過了,以至于所有編程語言(匯編除外)都把它掩藏起來,讓你在編程中感覺不到調(diào)用棧的存在。

          操作系統(tǒng)用昂貴的軟中斷機(jī)制實現(xiàn)了棧的保存和恢復(fù)。那有沒有別的方式實現(xiàn) Continuation 呢?最樸素的想法就是,把所有用得到的信息包成一個函數(shù)對象,在調(diào)用 g() 的時候一起傳進(jìn)去,并約定:一旦 g() 完成,就拿著結(jié)果去調(diào)用這個 Continuation。

          這種編程模式被稱為 Continuation-passing style(CPS):

          1. 把調(diào)用者 f() 還未執(zhí)行的部分包成一個函數(shù)對象 cont,一同傳給被調(diào)用者 g()
          2. 正常運行 g() 函數(shù)體;
          3. g() 完成后,連同它的結(jié)果一起回調(diào) cont,從而繼續(xù)執(zhí)行 f() 里剩余的代碼。

          再拿 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ù)帶一個額外的參數(shù):一個顯式的 Continuation,具體來說就是個僅有一個參數(shù)的函數(shù)。當(dāng) CPS 函數(shù)計算完返回值時,它“返回”的方式就是拿著返回值調(diào)用那個 Continuation。

          你應(yīng)該已經(jīng)發(fā)現(xiàn)了,這也就是回調(diào)函數(shù),我只是換了個名字而已。

          異步的樸素實現(xiàn):Callback

          光有回調(diào)函數(shù)其實并沒有卵用。對于純粹的計算工作,Call Stack 就很好,為何要費時費力用回調(diào)來做 Continuation 呢?你說的對,但僅限于沒有 IO 的情況。我們知道 IO 通常要比 CPU 慢上好幾個數(shù)量級,在 BIO 中,線程發(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 操作時也會一并輸入回調(diào)(也就是 Continuation),這大大解放了生產(chǎn)力——現(xiàn)場無需等待,可以立即返回去做其他事情。一旦 IO 成功后,AIO 的 Event Loop 會調(diào)用剛剛設(shè)置的回調(diào)函數(shù),把剩下的工作完成。這種模式有時也被稱為 Fire and Forget。

          recv_from_socket((input) -> {
                  var result = calculator.calculate(input)
                  send_to_socket(result) // ignore result
                  })

          就這么簡單,通過我們自己實現(xiàn)的 Continuation,線程不再受 IO 阻塞,可以自由自在地跑滿 CPU。

          一顆語法糖:Promise

          回調(diào)函數(shù)哪里都好,就是不大好用,以及太丑了。

          第一個問題是可讀性大大下降,由于我們繞開操作系統(tǒng)自制 Continuation,所有函數(shù)調(diào)用都要傳入一個 lambda 表達(dá)式,你的代碼看起來就像要起飛一樣,縮進(jìn)止不住地往右挪(the "Callback Hell")。

          第二個問題是各種細(xì)節(jié)處理起來很麻煩,比如,考慮下異常處理,看來傳一個 Continuation 還不夠,最好再傳個異常處理的 callback。

          Promise 是對異步調(diào)用結(jié)果的一個封裝 ,在 Java 中它叫作 CompletableFuture (JDK8) 或者 ListenableFuture (Guava)。Promise 有兩層含義:

          第一層含義是:我現(xiàn)在還不是真正的結(jié)果,但是承諾以后會拿到這個結(jié)果 。這很容易理解,異步的任務(wù)遲早會完成,調(diào)用者如果比較蠢萌,他也可以用 Promise.get() 強(qiáng)行要拿到結(jié)果,順便阻塞了當(dāng)前線程,異步變成了同步。

          第二層含義是:如果你(調(diào)用者)有什么吩咐,就告訴我好了 。這就有趣了,換句話說,回調(diào)函數(shù)不再是傳給 g(),而是 g() 返回的 Promise,比如之前那段代碼,我們用 Promise 來書寫,看起來順眼了不少。

          var promise_input = recv_from_socket()
                  promise_input.then((input) -> {
                  var result = calculator.calculate(input)
                  send_to_socket(result) // ignore result
                  })

          Promise 改善了 Callback 的可讀性,也讓異常處理稍稍優(yōu)雅了些,但終究是顆語法糖。

          反應(yīng)式編程

          反應(yīng)式(Reactive)最早源于函數(shù)式編程中的一種模式,隨著微軟發(fā)起 ReactiveX 項目并一步步壯大,被移植到各種語言和平臺上。Reactive 最初在 GUI 編程中有廣泛的應(yīng)用,由于異步調(diào)用的高性能,很快也在服務(wù)器后端領(lǐng)域遍地開花。

          Reactive 可以看作是對 Promise 的極大增強(qiáng),相比 Promise,反應(yīng)式引入了流(Flow)的概念 。ReactiveX 中的事件流從一個 Observable 對象流出,這個對象可以是一個按鈕,也可以是 Restful API,總之,它能被外界觸發(fā)。與 Promise 不同的是,事件可能被觸發(fā)多次,所以處理代碼也會被多次調(diào)用。

          一旦允許調(diào)用多次,從數(shù)據(jù)流動的角度看,事實上模型已經(jīng)是 Push 而非 Pull 。那么問題來了,如果調(diào)用頻率非常高,以至于我們處理速度跟不上了怎么辦?所以 RX 框架又引入了 Backpressure 機(jī)制來進(jìn)行流控,最簡單的流控方式就是:一旦 buffer 滿,就丟棄掉之后的事件。

          ReactiveX 框架的另一個優(yōu)點是內(nèi)置了很多好用的算子,比如:merge(Flow 合并),debounce(開關(guān)除顫)等等,方便了業(yè)務(wù)開發(fā)。下面是一個 RxJava 的例子:

          CPS 變換:Coroutine 與 async/await

          無論是反應(yīng)式還是 Promise,說到底仍然沒有擺脫手工構(gòu)造 Continuation:開發(fā)者要把業(yè)務(wù)邏輯寫成回調(diào)函數(shù)。對于線性的邏輯基本可以應(yīng)付自如,但是如果邏輯復(fù)雜一點呢?(比如,考慮下包含循環(huán)的情況)

          有些語言例如 C#,JavaScript 和 Python 提供了 async/await 關(guān)鍵字。與 Reactive 一樣,這同樣出自微軟 C# 語言。在這些語言中,你會感到前所未有的爽感:異步編程終于擺脫了回調(diào)函數(shù)!唯一要做的只是在異步函數(shù)調(diào)用時加上 await,編譯器就會自動把它轉(zhuǎn)化為協(xié)程(Coroutine),而非昂貴的線程。

          魔法的背后是 CPS 變換,CPS 變換把普通函數(shù)轉(zhuǎn)換成一個 CPS 的函數(shù),即 Continuation 也能作為一個調(diào)用參數(shù) 。函數(shù)不僅能從頭運行,還能根據(jù) Continuation 的指示繼續(xù)某個點(比如調(diào)用 IO 的地方)運行。

          例子可以參見我的下一篇文章。由于代碼太長,就不貼在這兒了。

          可以看到,函數(shù)已經(jīng)不再是一個函數(shù)了,而是變成一個狀態(tài)機(jī) 。每次 call 它、或者它 call 其他異步函數(shù)時,狀態(tài)機(jī)都會做一些計算和狀態(tài)輪轉(zhuǎn)。說好的 Continuation 在哪呢?就是對象自己(this)啊。

          CPS 變換實現(xiàn)非常復(fù)雜,尤其是考慮到 try-catch 之后。但是沒關(guān)系,復(fù)雜性都在編譯器里,用戶只要學(xué)兩個關(guān)鍵詞即可。這個特性非常優(yōu)雅,比 Java 那個廢柴的 CompletableFuture 不知道高到哪去了。(更新:也沒有那么廢柴啦)

          JVM 上也有一個實現(xiàn):electronicarts/ea-async,原理和 C# 的 async/await 類似,在編譯期修改 Bytecode 實現(xiàn) CPS 變換。

          終極方案:用戶態(tài)線程

          有了 async/await,代碼已經(jīng)簡潔很多了,基本上和同步代碼無異。是否有可能讓異步代碼和同步代碼完全一樣呢?聽起來就像免費午餐,但是的確可以做到!

          用戶態(tài)線程的代表是 Golang。JVM 上也有些實現(xiàn),比如 Quasar,不過因為 JDBC、Spring 這些周邊生態(tài)(它們占據(jù)了大部分 IO 操作)的缺失基本沒有什么用。

          用戶態(tài)線程是把操作系統(tǒng)提供的線程機(jī)制完全拋棄 ,換句話說,不去用這個 VM 的虛擬化機(jī)制。比如硬件有 8 個核心,那就創(chuàng)建 8 個系統(tǒng)線程,然后把 N 個用戶線程調(diào)度到這 8 個系統(tǒng)線程上跑。N 個用戶線程的調(diào)度在用戶進(jìn)程里實現(xiàn),由于一切都在進(jìn)程內(nèi)部,切換代價要遠(yuǎn)遠(yuǎn)小于操作系統(tǒng) Context Switch。

          另一方面,所有可能阻塞系統(tǒng)級線程的事情,例如 sleep()recv() 等,用戶態(tài)線程一定不能碰,否則它一旦阻塞住也就帶著那 8 個系統(tǒng)線程中的一個阻塞了。Go Runtime 接管了所有這樣的系統(tǒng)調(diào)用,并用一個統(tǒng)一的 Event loop 來輪詢和分發(fā)。

          另外,由于用戶態(tài)線程很輕量,我們完全沒必要再用線程池,如果需要開線程就直接創(chuàng)建。比如 Java 中的 WebServer 幾乎一定有個線程池,而 Go 可以給每個請求開辟一個 goroutine 去處理。并發(fā)編程從未如此美好!

          總結(jié)

          以上方案中,Promise、Reactive 本質(zhì)上還是回調(diào)函數(shù),只是框架的存在一定程度上降低了開發(fā)者的心智負(fù)擔(dān)。而 async/await 和用戶態(tài)線程的解決方案要優(yōu)雅和徹底的多,前者通過編譯期的 CPS 變換幫用戶創(chuàng)造出 CPS 式的函數(shù)調(diào)用;后者則繞開操作系統(tǒng)、重新實現(xiàn)一套線程機(jī)制,一切調(diào)度工作由 Runtime 接管。

          不知道是不是因為歷史包袱太重,Java 語言本身提供的異步編程支持弱得可憐,即便是 CompletableFuture 還是在 Java 8 才引入,其后果就是很多庫都沒有異步的支持。雖然 Quasar 在沒有語言級支持的情況下引入了 CPS 變換,但是由于缺少周邊生態(tài)的支持,實際很難用在項目中。

          歡迎添加程序汪個人微信 itwang007  進(jìn)粉絲群或圍觀朋友圈



          往期資源  需要請自取

          Java項目分享 最新整理全集,找項目不累啦 03版

          臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載

          字節(jié)跳動總結(jié)的設(shè)計模式 PDF 火了,完整版開放下載!

          堪稱神級的Spring Boot手冊,從基礎(chǔ)入門到實戰(zhàn)進(jìn)階

          臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!

          喜歡就"在看"唄^_^

          瀏覽 67
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www.一级内射 | 玖玖干| 蜜桃视频一区 | 日韩18成人久久久 | 久久香蕉网 |