無(wú)棧協(xié)程 | Rust學(xué)習(xí)筆記

作者:謝敬偉,江湖人稱“刀哥”搂鲫,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í)行entersyscallexitsyscall,就是這個(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扛点,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子岂丘,更是在濱河造成了極大的恐慌陵究,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奥帘,死亡現(xiàn)場(chǎng)離奇詭異铜邮,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)寨蹋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)松蒜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人已旧,你說(shuō)我怎么就攤上這事秸苗。” “怎么了运褪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵惊楼,是天一觀的道長(zhǎng)玖瘸。 經(jīng)常有香客問(wèn)我,道長(zhǎng)檀咙,這世上最難降的妖魔是什么雅倒? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮弧可,結(jié)果婚禮上蔑匣,老公的妹妹穿的比我還像新娘。我一直安慰自己棕诵,他們只是感情好殖演,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著年鸳,像睡著了一般趴久。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搔确,一...
    開(kāi)封第一講書(shū)人閱讀 49,784評(píng)論 1 290
  • 那天彼棍,我揣著相機(jī)與錄音,去河邊找鬼膳算。 笑死座硕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涕蜂。 我是一名探鬼主播华匾,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼机隙!你這毒婦竟也來(lái)了蜘拉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤有鹿,失蹤者是張志新(化名)和其女友劉穎旭旭,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體葱跋,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡持寄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了娱俺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稍味。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖荠卷,靈堂內(nèi)的尸體忽然破棺而出模庐,到底是詐尸還是另有隱情,我是刑警寧澤僵朗,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布赖欣,位于F島的核電站屑彻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏顶吮。R本人自食惡果不足惜社牲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悴了。 院中可真熱鬧搏恤,春花似錦、人聲如沸湃交。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)搞莺。三九已至息罗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間才沧,已是汗流浹背迈喉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留温圆,地道東北人挨摸。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像岁歉,于是被迫代替她去往敵國(guó)和親得运。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348