async-await:協作排程 vs 搶佔排程

banq發表於2022-01-26

執行緒是為了並行化計算密集型任務。然而,如今,許多應用程式都是 I/O(輸入/輸出)密集型應用程式。

這樣,執行緒就有兩個重大問題:

  • 他們使用大量(與其他解決方案相比)記憶體
  • 啟動和上下文切換的成本可以在大量(數萬個)執行緒執行時感受到。

在實踐中,這意味著通過使用執行緒,我們的應用程式將花費大量時間等待網路請求完成並使用比必要更多的資源。

從程式設計師的角度來看,async/await提供了與執行緒相同的東西:併發性、更好的硬體利用率、更高的速度,但對於 I/O 繫結的工作負載具有顯著更好的效能和更低的資源使用率。

什麼是I/O 繫結工作負載?這些任務大部分時間都在等待網路或磁碟操作完成,而不是受到處理器計算能力的限制。

執行緒是很久以前設計的,當時大多數計算都不是網路(Web)相關的東西,因此不適合太多併發 I/O 任務。

正如我們從 Jim Blandy 所做的這些測量中看到的那樣,使用非同步的上下文切換比使用 Linux 執行緒快大約 30%,並且使用的記憶體減少了大約 20 倍。

在程式語言世界中,處理 I/O 任務的方式主要有兩種:搶佔式排程和協作式排程。

 

搶先排程

搶佔式排程是指任務的排程不受開發人員控制,完全由執行時管理。無論程式設計師是啟動同步任務還是非同步任務,程式碼都沒有區別。

例如,Go程式設計依靠的是搶佔式排程。

它的優點是更容易學習:對於開發者來說,同步和非同步程式碼之間沒有區別。另外,它幾乎不可能被濫用:執行時負責處理一切。

下面是一個在Go中進行HTTP請求的例子:

resp, err := http.Get("http://kerkour.com")

僅僅通過看這個片段,我們無法判斷http.Get是I/O密集型還是計算密集型。

其缺點是。

  • 速度,受限於執行時Runtime的聰明程度。
  • 難以除錯bug。如果執行時有bug,可能極難發現,因為執行時被開發者當成了黑暗魔法。

 

合作排程

另一方面,在合作式排程下,開發者負責告訴執行時Runtime一個任務何時要花一些時間來等待I/O。

也就是要等待?是的,你明白了。這正是 await 關鍵字的目的。

這是給執行時(和編譯器)的指示,任務將花費一些時間來等待操作完成,因此計算資源可以在此期間用於其他任務。

它的優點是速度極快。基本上,開發者和執行時一起工作,和諧相處,以最大限度地利用處置時的計算能力。

合作排程的主要缺點是它更容易被濫用:如果一個等待被遺忘(幸運的是,Rust編譯器會發出警告),或者事件迴圈被阻塞超過幾微秒,就會對系統的效能產生災難性的影響。

其推論是,一個async 程式應該極其小心地處理計算密集型操作。

下面是一個用Rust進行HTTP請求的例子。

let res = reqwest::get("https://www.rust-lang.org").await?;

.await 關鍵字告訴我們,reqwest::get 函式預計需要一些時間來完成。

 

相關文章