作者:謝敬偉,江湖人稱“刀哥”搂鲫,20年IT老兵傍药,數(shù)據(jù)通信網(wǎng)絡(luò)專家,電信網(wǎng)絡(luò)架構(gòu)師魂仍,目前任Netwarps開(kāi)發(fā)總監(jiān)拐辽。刀哥在操作系統(tǒng)、網(wǎng)絡(luò)編程擦酌、高并發(fā)薛训、高吞吐、高可用性等領(lǐng)域有多年的實(shí)踐經(jīng)驗(yàn)仑氛,并對(duì)網(wǎng)絡(luò)及編程等方面的新技術(shù)有濃厚的興趣乙埃。
Rust
作為一門(mén)新興語(yǔ)言,主打系統(tǒng)編程锯岖。提供了多種編寫(xiě)代碼的模式介袜。2019年底正式推出了 async/await語(yǔ)法,標(biāo)志著Rust
也進(jìn)入了協(xié)程時(shí)代出吹。下面讓我們來(lái)看一看遇伞。Rust
協(xié)程和Go
協(xié)程究竟有什么不同。
有棧協(xié)程 vs. 無(wú)棧協(xié)程
協(xié)程的需求來(lái)自于C10K
問(wèn)題捶牢,這里不做更多探討鸠珠。早期解決此類問(wèn)題的辦法是依賴于操作系統(tǒng)提供的I/O
復(fù)用操作巍耗,也就是 epoll
/IOCP
多路復(fù)用加線程池技術(shù)來(lái)實(shí)現(xiàn)的。本質(zhì)上這類程序會(huì)維護(hù)一個(gè)復(fù)雜的狀態(tài)機(jī)渐排,采用異步的方式編碼炬太,消息機(jī)制或者是回調(diào)函數(shù)。很多用 C/C++
實(shí)現(xiàn)的框架都是這個(gè)套路驯耻,缺點(diǎn)在于這樣的代碼一般比較復(fù)雜亲族,特別是異步編碼加狀態(tài)機(jī)的模式對(duì)于程序員是一個(gè)很大的挑戰(zhàn)。但是從另外一個(gè)角度看可缚,符合人類邏輯思維的操作方式卻恰恰是同步的霎迫。
考慮一個(gè)web server的場(chǎng)景:每次一個(gè)連接一般是請(qǐng)求下載一些數(shù)據(jù),如果可以用一個(gè)線程來(lái)處理每一次新連接帘靡,那么這個(gè)內(nèi)部的代碼邏輯就可以用同步的方式一路寫(xiě)下來(lái):首先接收數(shù)據(jù)知给,然后完成HTTP request解析。根據(jù)HTTP頭部的信息訪問(wèn)數(shù)據(jù)庫(kù)描姚,然后將取得的結(jié)果封裝在HTTP response中炼鞠,返回給用戶,最后關(guān)閉連接轰胁。如果是這樣谒主,你會(huì)發(fā)現(xiàn)這里并不需要狀態(tài)機(jī),也沒(méi)有什么回調(diào)函數(shù)赃阀,很可能也不需要定時(shí)器霎肯,整個(gè)的過(guò)程就是一個(gè)流水賬,而這正是人類最容易理解的思維方式榛斯。然而观游,我們不能簡(jiǎn)單地用多線程來(lái)解決C10K
問(wèn)題,因?yàn)椴僮飨到y(tǒng)的線程資源是很有限的驮俗,而且是昂貴的懂缕。操作系統(tǒng)會(huì)限制可以打開(kāi)的線程數(shù),同時(shí)線程之間的切換開(kāi)銷也是比較大的王凑。
Go 有棧協(xié)程
Go
語(yǔ)言的出現(xiàn)提供了一種新的思路搪柑。Go
語(yǔ)言的協(xié)程則相當(dāng)于提供了一種很低成本的類似于多線程的執(zhí)行體。在Go
語(yǔ)言中索烹,協(xié)程的實(shí)現(xiàn)與操作系統(tǒng)多線程非常相似工碾。操作系統(tǒng)一般使用搶占的方式來(lái)調(diào)度系統(tǒng)中的多線程,而Go
語(yǔ)言中百姓,依托于操作系統(tǒng)的多線程渊额,在運(yùn)行時(shí)刻庫(kù)中實(shí)現(xiàn)了一個(gè)協(xié)作式的調(diào)度器。這里的調(diào)度真正實(shí)現(xiàn)了上下文的切換,簡(jiǎn)單地說(shuō)旬迹,Go
系統(tǒng)調(diào)用執(zhí)行時(shí)火惊,調(diào)度器可能會(huì)保存當(dāng)前執(zhí)行協(xié)程的上下文到堆棧中。然后將當(dāng)前協(xié)程設(shè)置為睡眠奔垦,轉(zhuǎn)而執(zhí)行其他的協(xié)程屹耐。這里需要注意,所謂的Go
系統(tǒng)調(diào)用并不是真正的操作系統(tǒng)的系統(tǒng)調(diào)用宴倍,而是Go
運(yùn)行時(shí)刻庫(kù)提供的對(duì)底層操作系統(tǒng)調(diào)用的一個(gè)封裝张症。舉例說(shuō)明:Socket recv仓技。我們知道這是一個(gè)系統(tǒng)調(diào)用鸵贬,Go
的運(yùn)行時(shí)刻庫(kù)也提供了幾乎一模一樣的調(diào)用方式,但這只是建立在 epoll
之上的模擬層脖捻,底層的socket是工作在非阻塞的方式阔逼,而模擬層提供給我們了看上去是阻塞模式的socket。讀寫(xiě)這個(gè)模擬的socket會(huì)進(jìn)入調(diào)度器地沮,最終導(dǎo)致協(xié)程切換嗜浮。目前Go
調(diào)度器實(shí)現(xiàn)在用戶空間,本質(zhì)上是一種協(xié)作式的調(diào)度器摩疑。這也是為什么如果寫(xiě)了一個(gè)死循環(huán)在協(xié)程里危融,則協(xié)程永遠(yuǎn)沒(méi)有機(jī)會(huì)被換出,一個(gè)Processor相當(dāng)于就被浪費(fèi)掉了雷袋。
有棧的協(xié)程和操作系統(tǒng)多線程是很相似的吉殃。考慮以下偽代碼:
func routine() int
{
var a = 5
sleep(1000)
a += 1
return a
}
sleep調(diào)用時(shí)楷怒,會(huì)發(fā)生上下文的切換蛋勺,當(dāng)前的執(zhí)行體被掛起,直到約定的時(shí)間再被喚醒鸠删。局部變量a 在切換時(shí)會(huì)被保存在棧中抱完,切換回來(lái)后從棧中恢復(fù),從而得以繼續(xù)運(yùn)行刃泡。所謂有棧就是指執(zhí)行體本身的棧巧娱。每次創(chuàng)建一個(gè)協(xié)程,需要為它分配椇嫣空間家卖。究竟分配多大的棧的空間是一個(gè)技術(shù)活。分的多了庙楚,浪費(fèi)上荡,分的少了,可能會(huì)溢出。Go
在這里實(shí)現(xiàn)了一個(gè)協(xié)程棧擴(kuò)容的機(jī)制酪捡,相對(duì)比較優(yōu)雅的解決了這個(gè)問(wèn)題叁征。另外一個(gè)問(wèn)題,關(guān)于上下文切換逛薇,這一般是跟平臺(tái)或者CPU相關(guān)的代碼捺疼,因?yàn)橐婕暗郊拇嫫鞑僮鳌M瑫r(shí)上下文切換也是有一點(diǎn)代價(jià)的永罚,因?yàn)楫吘剐枰~外執(zhí)行一些指令(個(gè)人覺(jué)得這一點(diǎn)可以忽略掉啤呼,無(wú)棧的協(xié)程實(shí)現(xiàn)難道不是也需要一些額外的指令來(lái)完成程序邏輯的跳轉(zhuǎn)?)呢袱。
有棧協(xié)程看起來(lái)還是比較直觀官扣,特別是對(duì)于開(kāi)發(fā)人員比較友好。如果對(duì)比一下Rust
實(shí)現(xiàn)的無(wú)棧協(xié)程羞福,就會(huì)知道因?yàn)橐脒@個(gè)棧惕蹄,保存上下文,從而解決了很多很麻煩的問(wèn)題治专。
關(guān)于Go
卖陵,講一點(diǎn)題外話。
Go
有一個(gè)比較龐大的運(yùn)行時(shí)刻庫(kù)张峰。從上文我們了解到泪蔫,因?yàn)?code>Go調(diào)度器的需要,運(yùn)行時(shí)刻庫(kù)把所有的系統(tǒng)調(diào)用都做了封裝喘批,這些所謂系統(tǒng)調(diào)用都被引入了調(diào)度器的調(diào)度點(diǎn)撩荣,也就是說(shuō),執(zhí)行這類系統(tǒng)調(diào)用會(huì)進(jìn)行協(xié)程的上下文切換谤祖。所以換一句話說(shuō)婿滓。Go
的系統(tǒng)調(diào)用,其實(shí)都是被包裝過(guò)的粥喜,能夠感知協(xié)程的系統(tǒng)調(diào)用凸主。所以從這個(gè)角度也可以理解為什么Go
的運(yùn)行時(shí)刻庫(kù)是比較龐大的。另外额湘,cgo
的執(zhí)行也是類似的過(guò)程卿吐。因?yàn)檎{(diào)用的C代碼非常有可能通過(guò)C
庫(kù)來(lái)執(zhí)行系統(tǒng)調(diào)用,這樣會(huì)使線程進(jìn)入阻塞锋华,從而影響Go
的調(diào)度器的行為嗡官。所以我們看到cgo
總會(huì)執(zhí)行entersyscall
和exitsyscall
,就是這個(gè)原因毯焕。
Rust 協(xié)程
綠色線程 GreenThread
早期的Rust
支持一個(gè)所謂的綠色線程衍腥,其實(shí)就是有棧協(xié)程的實(shí)現(xiàn)磺樱,與Go
協(xié)程實(shí)現(xiàn)很相似。在0.7之后婆咸,綠色線程就被刪除了竹捉。其中一個(gè)原因是,如果引入這樣的機(jī)制尚骄,那么運(yùn)行時(shí)刻庫(kù)也必須如Go
語(yǔ)言一樣能夠支持有棧協(xié)程块差,也就是之前討論Go
題外話提到的內(nèi)容。Go
沒(méi)有Native thread的概念倔丈,語(yǔ)言層面只支持協(xié)程憨闰,選擇封裝全部的系統(tǒng)調(diào)用很合理。然而需五,如果Rust
也打算這么做鹉动,那么Native thread和協(xié)程運(yùn)行庫(kù)API
統(tǒng)一的問(wèn)題將很難解決。
無(wú)棧協(xié)程
無(wú)棧協(xié)程顧名思義就是不使用棧和上下文切換來(lái)執(zhí)行異步代碼邏輯的機(jī)制警儒。這里異步代碼雖然是異步的训裆,但執(zhí)行起來(lái)看起來(lái)是一個(gè)同步的過(guò)程眶根。從這一點(diǎn)上來(lái)看Rust
協(xié)程與Go
協(xié)程也沒(méi)什么兩樣蜀铲。舉例說(shuō)明:
async fn routine()
{
let mut a = 5;
sleep(1000).await;
a = a + 1;
a
}
幾乎是一樣的流程。Sleep會(huì)導(dǎo)致睡眠属百,當(dāng)時(shí)間已到记劝,重新返回執(zhí)行,局部變量a 內(nèi)容應(yīng)該還是5族扰。Go
協(xié)程是有棧的厌丑,所以這個(gè)局部變量保存在棧中,而Rust
是怎么實(shí)現(xiàn)的呢渔呵?答案就是 Generator 生成的狀態(tài)機(jī)怒竿。Generator 和閉包類似,能夠捕獲變量a扩氢,放入一個(gè)匿名的結(jié)構(gòu)中耕驰,在代碼中看起來(lái)是局部變量的數(shù)據(jù) a,會(huì)被放入結(jié)構(gòu)录豺,保存在全局(線程)棧中朦肘。另外值得一提的是,Generator 生成了一個(gè)狀態(tài)機(jī)以保證代碼正確的流程双饥。從sleep.await 返回之后會(huì)執(zhí)行 a=a+1 這行代碼媒抠。async routine() 會(huì)根據(jù)內(nèi)部的 .await 調(diào)用生成這樣的狀態(tài)機(jī),驅(qū)動(dòng)代碼按照既定的流程去執(zhí)行咏花。
按照一般的說(shuō)法趴生。無(wú)棧協(xié)程有很多好處。首先不用分配棧。因?yàn)榫烤菇o協(xié)程分配多大的棧是個(gè)大問(wèn)題苍匆。特別是在32位的系統(tǒng)下舍咖,地址空間是有限的。每個(gè)協(xié)程都需要專門(mén)的棧锉桑,很明顯會(huì)影響到可以創(chuàng)建的協(xié)程總數(shù)排霉。其次,沒(méi)有上下文切換民轴,貌似性能也許會(huì)好一些攻柠?當(dāng)然,更大的好處是并不需要與CPU體系相關(guān)代碼后裸,也就有了更好的跨平臺(tái)的能力瑰钮。當(dāng)然,無(wú)棧問(wèn)題也不少微驶。例如浪谴,Rust
著名的PIN問(wèn)題。另外因苹,個(gè)人覺(jué)得Rust
的無(wú)棧協(xié)程主要問(wèn)題是不那么直觀苟耻,理解起來(lái)會(huì)稍微吃力一些。
協(xié)程解決的問(wèn)題
Rust
語(yǔ)言真正實(shí)現(xiàn) async/await 語(yǔ)法只是去年底的事情扶檐。在那之前凶杖,有一些其他臨時(shí)使用宏的替代做法。所以現(xiàn)在去看一些開(kāi)源的軟件項(xiàng)目款筑,真正采用 await 寫(xiě)代碼還是很少的智蝠,主要是 poll 的方式,這樣的代碼需要自己維護(hù)各種狀態(tài)奈梳。一個(gè)經(jīng)典的例子就是Sink發(fā)送的三件套:poll_ready/start_send/poll_flush杈湾,首先需要檢查是否緩沖區(qū)有待發(fā)送的數(shù)據(jù),若是攘须,則優(yōu)先處理這一部分?jǐn)?shù)據(jù)漆撞。然后檢查底層是否就緒,否則無(wú)法發(fā)送阻课,這時(shí)候需要把當(dāng)前發(fā)送的東西轉(zhuǎn)存下來(lái)叫挟,也就是前面提到的發(fā)送緩沖區(qū)。如果用C語(yǔ)言寫(xiě)過(guò)epoll 相關(guān)的代碼限煞,那么會(huì)發(fā)現(xiàn)和這里也沒(méi)有什么大的區(qū)別抹恳。因?yàn)檫@就是異步編程大致的模式。而事實(shí)上署驻,如果可以用await
來(lái)寫(xiě)代碼奋献,直接調(diào)用SinkExt的send().await方法量承,一切煩惱都消失了肘交。SinkExt::send 內(nèi)部實(shí)現(xiàn)了包含發(fā)送緩沖的Sink的三件套,而await
用一種簡(jiǎn)潔的方式將這一切優(yōu)雅地呈現(xiàn)出來(lái)。這種利用.await 寫(xiě)出來(lái)的代碼浴栽,看似是用同步的方式在做異步的編程咬像,比較簡(jiǎn)潔城舞,易于理解槽华。
總之,個(gè)人覺(jué)得Rust
異步編程的未來(lái)是 await
杭攻。早期手動(dòng)來(lái)寫(xiě)各種poll方法祟敛,實(shí)在是太繁瑣了。語(yǔ)言實(shí)則是一種工具兆解,被發(fā)明出來(lái)是用來(lái)幫助程序員的馆铁,而不是造成更多的負(fù)擔(dān)。我相信這也是Rust
.await 最大的意義锅睛。
下一篇文章埠巨,我們來(lái)研究下 async/await 究竟做了什么。
深圳星鏈網(wǎng)科科技有限公司(Netwarps)现拒,專注于互聯(lián)網(wǎng)安全存儲(chǔ)領(lǐng)域技術(shù)的研發(fā)與應(yīng)用辣垒,是先進(jìn)的安全存儲(chǔ)基礎(chǔ)設(shè)施提供商,主要產(chǎn)品有去中心化文件系統(tǒng)(DFS)具练、企業(yè)聯(lián)盟鏈平臺(tái)(EAC)乍构、區(qū)塊鏈操作系統(tǒng)(BOS)甜无。
微信公眾號(hào):Netwarps