Rust 異步:搶佔式調度 vs 協作式調度
線程被設計用來併發的執行計算密集型任務。然而,現在很多應用程序都是 I/O 密集型的。因此,線程就存在兩個重要的問題:
-
它們使用大量內存
-
啓動和上下文切換有一定的成本,當運行大量 (數以萬計) 線程時可以感受到。
在實踐中,這意味着通過使用線程,我們的應用程序將花費大量時間等待網絡請求完成,並使用大量不必要的資源。
線程的問題
從程序員的角度來看,async/await 提供了與線程相同的東西:併發,更好的硬件利用率,提高速度。但對於與 I/O 相關的工作負載,異步具有顯著的性能提升和較低的資源佔用。
什麼是 I/O 工作負載?這些任務大部分時間都在等待網絡或磁盤操作的完成,而不是受到處理器計算能力的限制。
線程是很久以前設計的,當時大多數計算與網絡 (web) 無關,因此不適合太多的併發 I/O 任務。
從 Jim Blandy 所做的這些測量中看到,使用異步的上下文切換速度大約是使用 Linux 線程的 8.5 倍,使用的內存大約是比使用 Linux 線程少 20 倍。
在編程語言的世界中,處理 I/O 任務主要有兩種方法:搶佔式調度和協作式調度。
搶佔式調度
搶佔式調度是指任務的調度不受開發人員控制,完全由運行時管理。無論程序員啓動的是同步任務還是異步任務,代碼中沒有區別。
例如,Go 語言依賴於搶佔式調度。
它的優點是更容易學習:對於開發人員來說,同步和異步代碼之間沒有區別。而且,它幾乎不可能被誤用:運行時負責處理所有事情。
下面是一個在 Go 中發出 HTTP 請求的例子:
resp, err := http.Get("https://www.rust-lang.org")
僅僅通過看這個片段,我們無法判斷 http.Get 是否是 I/O 密集型或計算密集型。
缺點是:
-
速度,這是受限於運行時的智能程度。
-
很難調試 bug:如果運行時有 bug,可能很難找到它,因爲開發人員將運行時視爲黑魔法。
協作式調度
另一方面,在協作式調度中,開發人員負責告知運行時任務何時需要花費一些時間等待 I/O。這正是 await 關鍵字的目的。這是給運行時 (和編譯器) 的一個指示,任務將需要一些時間來等待一個操作完成,這樣,計算資源就可以同時用於另一項任務。
它的優點是速度極快。基本上,開發人員和運行時是一起工作的,協調一致的,以充分利用分配的計算能力。
協作式調度的主要缺點是它更容易被誤用:如果忘記了 await(幸運的是,Rust 編譯器發出警告),或者如果事件循環被阻塞超過幾微秒,它就會對系統的性能產生災難性的影響。
由此得出的結論是,異步程序應該非常小心地處理計算密集型操作。
下面是一個在 Rust 中發出 HTTP 請求的例子:
let res = reqwest::get("https://www.rust-lang.org").await?;
await 關鍵字告訴我們,預計 reqwest::get 函數需要一段時間才能完成。這時計算資源可以處理其他任務。
本文翻譯自:
https://kerkour.com/cooperative-vs-preemptive-scheduling
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/MgluEx1Usc6WViUo2JdjDQ