做者:謝敬偉,江湖人稱「刀哥」,20年IT老兵,數據通訊網絡專家,電信網絡架構師,目前任Netwarps開發總監。刀哥在操做系統、網絡編程、高併發、高吞吐、高可用性等領域有多年的實踐經驗,並對網絡及編程等方面的新技術有濃厚的興趣。程序員
Rust
做爲一門新興語言,主打系統編程。提供了多種編寫代碼的模式。2019年末正式推出了 async/await語法,標誌着Rust
也進入了協程時代。下面讓咱們來看一看。Rust
協程和Go
協程究竟有什麼不一樣。web
有棧協程 vs. 無棧協程
協程的需求來自於C10K
問題,這裏不作更多探討。早期解決此類問題的辦法是依賴於操做系統提供的I/O
複用操做,也就是 epoll
/IOCP
多路複用加線程池技術來實現的。本質上這類程序會維護一個複雜的狀態機,採用異步的方式編碼,消息機制或者是回調函數。不少用 C/C++
實現的框架都是這個套路,缺點在於這樣的代碼通常比較複雜,特別是異步編碼加狀態機的模式對於程序員是一個很大的挑戰。可是從另一個角度看,符合人類邏輯思惟的操做方式卻偏偏是同步的。數據庫
考慮一個web server的場景:每次一個鏈接通常是請求下載一些數據,若是能夠用一個線程來處理每一次新鏈接,那麼這個內部的代碼邏輯就能夠用同步的方式一路寫下來:首先接收數據,而後完成HTTP request解析。根據HTTP頭部的信息訪問數據庫,而後將取得的結果封裝在HTTP response中,返回給用戶,最後關閉鏈接。若是是這樣,你會發現這裏並不須要狀態機,也沒有什麼回調函數,極可能也不須要定時器,整個的過程就是一個流水帳,而這正是人類最容易理解的思惟方式。然而,咱們不能簡單地用多線程來解決C10K
問題,由於操做系統的線程資源是頗有限的,並且是昂貴的。操做系統會限制能夠打開的線程數,同時線程之間的切換開銷也是比較大的。編程
Go 有棧協程
Go
語言的出現提供了一種新的思路。Go
語言的協程則至關於提供了一種很低成本的相似於多線程的執行體。在Go
語言中,協程的實現與操做系統多線程很是類似。操做系統通常使用搶佔的方式來調度系統中的多線程,而Go
語言中,依託於操做系統的多線程,在運行時刻庫中實現了一個協做式的調度器。這裏的調度真正實現了上下文的切換,簡單地說,Go
系統調用執行時,調度器可能會保存當前執行協程的上下文到堆棧中。而後將當前協程設置爲睡眠,轉而執行其餘的協程。這裏須要注意,所謂的Go
系統調用並非真正的操做系統的系統調用,而是Go
運行時刻庫提供的對底層操做系統調用的一個封裝。舉例說明:Socket recv。咱們知道這是一個系統調用,Go
的運行時刻庫也提供了幾乎如出一轍的調用方式,但這只是創建在 epoll
之上的模擬層,底層的socket是工做在非阻塞的方式,而模擬層提供給咱們了看上去是阻塞模式的socket。讀寫這個模擬的socket會進入調度器,最終致使協程切換。目前Go
調度器實如今用戶空間,本質上是一種協做式的調度器。這也是爲何若是寫了一個死循環在協程裏,則協程永遠沒有機會被換出,一個Processor至關於就被浪費掉了。安全
有棧的協程和操做系統多線程是很類似的。考慮如下僞代碼:微信
func routine() int { var a = 5 sleep(1000) a += 1 return a }
sleep調用時,會發生上下文的切換,當前的執行體被掛起,直到約定的時間再被喚醒。局部變量a 在切換時會被保存在棧中,切換回來後從棧中恢復,從而得以繼續運行。所謂有棧就是指執行體自己的棧。每次建立一個協程,須要爲它分配棧空間。究竟分配多大的棧的空間是一個技術活。分的多了,浪費,分的少了,可能會溢出。Go
在這裏實現了一個協程棧擴容的機制,相對比較優雅的解決了這個問題。另一個問題,關於上下文切換,這通常是跟平臺或者CPU相關的代碼,由於要涉及到寄存器操做。同時上下文切換也是有一點代價的,由於畢竟須要額外執行一些指令(我的以爲這一點能夠忽略掉,無棧的協程實現難道不是也須要一些額外的指令來完成程序邏輯的跳轉?)。網絡
有棧協程看起來仍是比較直觀,特別是對於開發人員比較友好。若是對比一下Rust
實現的無棧協程,就會知道由於引入這個棧,保存上下文,從而解決了不少很麻煩的問題。多線程
關於Go
,講一點題外話。閉包
Go
有一個比較龐大的運行時刻庫。從上文咱們瞭解到,由於Go
調度器的須要,運行時刻庫把全部的系統調用都作了封裝,這些所謂系統調用都被引入了調度器的調度點,也就是說,執行這類系統調用會進行協程的上下文切換。因此換一句話說。Go
的系統調用,其實都是被包裝過的,可以感知協程的系統調用。因此從這個角度也能夠理解爲何Go
的運行時刻庫是比較龐大的。另外,cgo
的執行也是相似的過程。由於調用的C代碼很是有可能經過C
庫來執行系統調用,這樣會使線程進入阻塞,從而影響Go
的調度器的行爲。因此咱們看到cgo
總會執行entersyscall
和exitsyscall
,就是這個緣由。架構
Rust 協程
綠色線程 GreenThread
早期的Rust
支持一個所謂的綠色線程,其實就是有棧協程的實現,與Go
協程實現很類似。在0.7以後,綠色線程就被刪除了。其中一個緣由是,若是引入這樣的機制,那麼運行時刻庫也必須如Go
語言同樣可以支持有棧協程,也就是以前討論Go
題外話提到的內容。Go
沒有Native thread的概念,語言層面只支持協程,選擇封裝所有的系統調用很合理。然而,若是Rust
也打算這麼作,那麼Native thread和協程運行庫API
統一的問題將很難解決。
無棧協程
無棧協程顧名思義就是不使用棧和上下文切換來執行異步代碼邏輯的機制。這裏異步代碼雖然是異步的,但執行起來看起來是一個同步的過程。從這一點上來看Rust
協程與Go
協程也沒什麼兩樣。舉例說明:
async fn routine() { let mut a = 5; sleep(1000).await; a = a + 1; a }
幾乎是同樣的流程。Sleep會致使睡眠,當時間已到,從新返回執行,局部變量a 內容應該仍是5。Go
協程是有棧的,因此這個局部變量保存在棧中,而Rust
是怎麼實現的呢?答案就是 Generator 生成的狀態機。Generator 和閉包相似,可以捕獲變量a,放入一個匿名的結構中,在代碼中看起來是局部變量的數據 a,會被放入結構,保存在全局(線程)棧中。另外值得一提的是,Generator 生成了一個狀態機以保證代碼正確的流程。從sleep.await 返回以後會執行 a=a+1 這行代碼。async routine() 會根據內部的 .await 調用生成這樣的狀態機,驅動代碼按照既定的流程去執行。
按照通常的說法。無棧協程有不少好處。首先不用分配棧。由於究竟給協程分配多大的棧是個大問題。特別是在32位的系統下,地址空間是有限的。每一個協程都須要專門的棧,很明顯會影響到能夠建立的協程總數。其次,沒有上下文切換,貌似性能也許會好一些?固然,更大的好處是並不須要與CPU體系相關代碼,也就有了更好的跨平臺的能力。固然,無棧問題也很多。例如,Rust
著名的PIN問題。另外,我的以爲Rust
的無棧協程主要問題是不那麼直觀,理解起來會稍微吃力一些。
協程解決的問題
Rust
語言真正實現 async/await 語法只是去年末的事情。在那以前,有一些其餘臨時使用宏的替代作法。因此如今去看一些開源的軟件項目,真正採用 await 寫代碼仍是不多的,主要是 poll 的方式,這樣的代碼須要本身維護各類狀態。一個經典的例子就是Sink發送的三件套:poll_ready/start_send/poll_flush,首先須要檢查是否緩衝區有待發送的數據,如果,則優先處理這一部分數據。而後檢查底層是否就緒,不然沒法發送,這時候須要把當前發送的東西轉存下來,也就是前面提到的發送緩衝區。若是用C語言寫過epoll 相關的代碼,那麼會發現和這裏也沒有什麼大的區別。由於這就是異步編程大體的模式。而事實上,若是能夠用await
來寫代碼,直接調用SinkExt的send().await方法,一切煩惱都消失了。SinkExt::send 內部實現了包含發送緩衝的Sink的三件套,而await
用一種簡潔的方式將這一切優雅地呈現出來。這種利用.await 寫出來的代碼,看似是用同步的方式在作異步的編程,比較簡潔,易於理解。
總之,我的以爲Rust
異步編程的將來是 await
。早期手動來寫各類poll方法,實在是太繁瑣了。語言實則是一種工具,被髮明出來是用來幫助程序員的,而不是形成更多的負擔。我相信這也是Rust
.await 最大的意義。
下一篇文章,咱們來研究下 async/await 究竟作了什麼。
深圳星鏈網科科技有限公司(Netwarps),專一於互聯網安全存儲領域技術的研發與應用,是先進的安全存儲基礎設施提供商,主要產品有去中心化文件系統(DFS)、企業聯盟鏈平臺(EAC)、區塊鏈操做系統(BOS)。
微信公衆號:Netwarps