Rust 的併發模型 vs Go 的併發模型:Stackless 協程 vs Stackfull 協程
雖然 Rust 和 Go 都是從上一代編程語言的錯誤中吸取教訓的現代編程語言,但它們以完全不同的方式管理併發,這對性能和開發人員體驗有巨大的影響。
但首先,我們爲什麼需要併發?
今天,大多數程序與需要一定時間才能返回響應的資源進行交互:例如網絡或磁盤。如果我們在等待網絡響應的同時完全阻塞程序的執行,這將是對硬件的一種相當低效的使用!
這就是爲什麼 Go 和 Rust 在等待 I/O(輸入 / 輸出) 時允許程序執行其他任務的語言特性。
任務
任務是可以併發執行的抽象計算單元:多個函數可以 (由程序) 同時處理,但它們不一定 (由 CPU) 同時執行(它的並行性需要多個線程)。
可以使用 go 關鍵字在 Go 中生成新任務:
go doSomething()
go doAnotherThing()
在 Rust 中,需要使用 spawn 函數:
tokio::spawn(async move {
do_something().await
});
tokio::spawn(async move {
do_another_thing().await
});
在這兩種情況下,任務都由語言的運行時同時處理。
運行時
運行時的目的是管理和調度不同的任務,以便有效地使用硬件。
Rust 和 Go 的第一個不同之處。你不能改變 Go 運行時 (除非你使用一個完全不同的編譯器,比如 tinygo),它是內置在語言中的,而在 Rust 中,語言沒有提供運行時,你必須自己配置。
函數在等待某些東西 (例如網絡) 時將控制權交還給運行時。在 Go 中,這是由標準庫、語言和編譯器自動完成的,而在 Rust 中,它在到達 await 關鍵字時發生。
Stackfull 協程
Stackfull 協程又稱綠線程,或 M:N 線程 (M 個綠線程運行在 N 個內核線程上) 是 Go 採用的併發模型。
在這個模型中,運行時管理輕量級 (綠色) 線程,並將它們調度到可用的硬件線程上。與內核線程一樣,每個任務都有自己的棧,如果需要,可以由運行時增加棧。
stackfull 協程的第一個問題是,每個任務都有自己的棧,這意味着每個任務使用較少的內存量。從 Go 1.22 開始,線程程序使用的最小內存量是 2 KiB,這意味着如果有 10,000 個併發任務在運行,程序將使用至少 20 MiB 的內存。
Stackfull 協程的第二個問題是,運行時需要完全控制棧佈局,這使得與其他語言 (如 C 的 FFI) 的互操作性變得困難,因爲運行時必須在能夠調用 C 代碼之前做一些準備棧的工作。這就是爲什麼 CGO 被認爲是緩慢的(在現實中,CGO 調用在 30 到 75 納秒內完成,在我看來這是相當快的)。
Stackless 協程
另一方面,Rust 採用了無棧協程方法,其中任務沒有自己的棧。在 Rust 中,Future 基本上是實現 Future Trait 的簡單結構,其中每個. await 調用鏈被編譯成巨大的狀態機。
如果你正在用 Python 或 c# 開發,你可能已經知道 async/await 函數着色的巨大代價,其中同步函數不能調用 async 函數,反之亦然。
這就導致了許多問題,比如導致了生態系統的碎片化,其中的庫是不可互操作的,很難在程序中使用 libA,因爲你使用的是 async 而不是這個庫,而且還導致了開發人員的許多錯誤,他們阻塞了運行時的事件循環,降低了系統的性能。
這在 Rust 中也同樣存在,因爲標準庫不提供與同步函數相同的異步函數 (例如 read 讀取整個文件),並且因爲不同的運行時甚至不能相互操作,如果你開始爲 tokio 運行時編寫程序,你將很難將其移植到另一個運行時。
雖然這些都在 Go 中得到了解決,在 Go 中,一切都是同步的,編譯器和運行時在調用程序員看不見的異步函數時自動插入等待點,但這是以性能損失 (內存和 CPU) 爲代價的。
雖然 Rust 方法可以最大限度地利用機器,但它帶來了一個碎片化的生態系統,這給 Rust 的採用帶來了很大的麻煩。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/tf6ulOwiHKYejy8kVz7JEw