Goroutines 是 Go 語言中用於實現協作式多任務處理的機制,使得 Go 成為一種既簡單又高效的並發編程語言。
最關鍵的是這東西可比教給小白什麼是 Async 方便多了
大家別沒事兒找事兒反駁我 Go 不好學,我昨天剛剛讓一個完全沒有 Go 經驗的人在兩個小時內學會了 Go
在 Go 中,每個 goroutine 都擁有自己的獨立棧,可以存儲臨時值和返回地址等信息。事實上,Go 編譯器在每個 goroutine 開始時都會分配一個特別小的棧,並根據需要進行擴展。
下面我就來解釋下為什麼 go
和 async 其實本質上在做一個事情
我們拿 Go 語言的一個實例 TinyGo 對 goroutines 的實現為例。它基於類似於 C#、JavaScript 和現在也用於 C++ 的異步 / 等待(async/await)模型。事實上,TinyGo好像看起來會自動插入 async/await 關鍵字,因為 TinyGo 是單線程的。
讓我們以以下代碼片段作為示例:
func main() {
go background()
time.Sleep(2 * time.Second)
println("some other operation")
n := compute()
println("done:", n)
}
func background() {
for {
println("background operation...")
time.Sleep(time.Second)
}
}
func compute() int {
time.Sleep(time.Second)
println("blocking operation completed")
return 42
}
如果我們人肉給他加上 async await 換成 JS 人可能會更熟悉的語法
async func main() {
go background()
await time.Sleep(2 * time.Second)
println("some other operation")
n := await compute()
println("done:", n)
}
async func background() {
for {
println("background operation...")
await time.Sleep(time.Second)
}
}
async func compute() int {
await time.Sleep(time.Second)
println("blocking operation completed")
return 42
}
我相信絕大多數用戶到這裡還沒有問題,但是別急,我們真的不需要學 async await。
我要引入 Delimited Continuation 了#
如果看不懂請查看文章 Delimited Continuation
Delimited Continuation(被限定延續)是一種編程模型,用於控制程序的執行流程,並在需要的時候保存和恢復執行的狀態。它提供了一種靈活而強大的控制流轉移機制,可以減輕開發者的心智負擔。
在傳統的控制流中,函數的執行順序是線性的,每個函數都有一個入口和一個出口。當一個函數調用另一個函數時,控制權會轉移到被調用函數,當被調用函數完成時,控制權返回給調用函數。這種線性控制流並不適用於一些複雜的編程需求,例如在異步編程中的回調地獄、協程的實現等。
被限定延續允許在函數的任意位置保存當前的執行狀態,並將控制權轉移到另一個代碼片段。這個代碼片段被稱為延續,它可以是一個閉包、一個函數或其他可執行的代碼塊。被限定延續提供了一種將執行狀態攜帶到不同的代碼片段的能力,從而實現了非局部的控制流轉移。
與被限定延續相關的是 Async Await 模型,它是一種用於簡化異步操作的編程模型,廣泛應用於現代異步編程。Async Await 允許開發者以同步的方式編寫異步代碼,而無需顯式地處理回調函數。它通過暫停和恢復函數的執行來等待異步操作的完成,在異步操作完成後繼續執行後續的代碼。
被限定延續和 Async Await 之間存在關聯和共享的概念。Async Await 實際上是基於被限定延續實現的異步編程模型。當使用 Await 暫停異步函數時,實際上是將當前的執行狀態保存到被限定延續中,並在異步操作完成後恢復執行。
利用被限定延續和編譯器自動實現轉換的好處是能夠大大減輕開發者的心智負擔。通過自動處理被限定延續的轉換,開發者不需要手動處理複雜的控制流操作,使得代碼更加簡潔、可讀性更高。這種轉換的自動化能夠提供更高級別的抽象,讓開發者專注於業務邏輯而不是底層的控制流細節。
自動處理被限定延續轉換的好處還包括:
-
簡化異步編程: 使用被限定延續可以簡化異步編程模型,使得編寫異步代碼更加直觀和易於理解。開發者可以像編寫同步代碼一樣使用異步功能,而不再需要處理複雜的回調函數和嵌套。
-
減少錯誤和提高可維護性: 自動處理被限定延續轉換可以減少編碼中的錯誤,並提高代碼的可維護性。由於這種轉換是由編譯器自動完成的,因此不容易出現手動處理控制流問題時常見的錯誤。
-
簡化複雜的控制流: 被限定延續提供了更靈活和細粒度的控制流轉移,使得處理複雜的控制流模式變得更容易。開發者可以使用被限定延續實現更高級別的控制結構,如協程、狀態機等。
大家反應過來沒有#
Go 語言最大的優點是整個語言成線性,沒有奇葩的控制流(假設你不 recover),整個語言就一個並發手段也就是 go
那麼,我們是不是很簡單的就能把他轉為 delimited continuation 格式呢?
假設我的編譯器有一個 pass 可以將 go 代碼根據控制流轉換成 delimited continuation 格式
這個 pass 非常簡單,因為從 ast 轉換為 cps 形式的 ast 我們淫王老師 100 行代碼就能搞定
我們會得到代碼
func main() {
background(func() {
time.Sleep(2 * time.Second)
println("some other operation")
compute(func(n int) {
println("done:", n)
})
})
}
func background(k func()) {
for {
println("background operation...")
time.Sleep(time.Second)
k()
}
}
func compute(k func(int)) {
time.Sleep(time.Second)
println("blocking operation completed")
k(42)
}
然後再通過一個 pass 把這種 trivial 的 delimited continuation 翻譯到 LLVM 的 coroutine
感謝 C 艹老鐵不斷堆特性,讓 LLVM 有了這麼好用的功能
func main(parent *coroutine) { // note: parent is always nil because this is main
hdl := llvm.makeCoroutine() // my coroutine
background(nil) // not passing a parent as it is a new independent goroutine
runtime.sleepTask(hdl, 2 * time.Second) // mark this function as sleeping
llvm.suspend(hdl) // suspend this coroutine
println("some other operation")
compute(hdl) // continuation-passing style
llvm.suspend(hdl)
n := hdl.data
println("done:", n)
runtime.resumeTask(parent) // re-activate parent (unnecessary)
}
func background(parent *coroutine) {
hdl := llvm.makeCoroutine()
for {
println("background operation...")
runtime.sleepTask(hdl, time.Second) // mark this function as sleeping
llvm.suspend(hdl)
}
// code is unreachable so there is no runtime.resumeTask call.
}
func compute(parent *coroutine) {
hdl := llvm.makeCoroutine()
runtime.sleepTask(hdl, time.Second) // mark this function as sleeping
llvm.suspend(hdl)
println("blocking operation completed")
parent.data = 42 // note: not yet implemented
runtime.resumeTask(parent) // re-activate parent
}
然後我們最後把 background 的 hdl spawn 在某個 task queue 就完成了整個過程
為什麼我們說 Go 贏了兩次?#
轉換為 CPS 的 delimited continuation 困難程度:#
轉換為 CPS 的 delimited continuation 將整個程序轉換為以延續函數形式傳遞控制流。Go 語言中,只有go
函數和<-
兩個個控制流操作,因此將代碼轉換為 CPS 風格相對較簡單。
實現自動染色的 Async Await 困難程度:#
實現自動染色的 Async Await 需要考慮編譯器和運行時環境的複雜性。這種自動化轉換需要編譯器對代碼進行靜態分析和轉換,將異步操作標記為掛起點,並在適當的時候恢復執行。編譯器需要進行語法分析、語義分析以及生成相應的中間表示或目標代碼。
實現自動染色的 Async Await 具有較高的技術複雜度。這個 pass 需要處理複雜的控制流、代碼生成和優化問題。這也需要對編譯器和底層語言的運行機制有深入的理解。
運行時消耗的資源情況#
在運行時,自動染色的 Async Await 模型通常需要維護額外的狀態,如異步任務的狀態、掛起點的上下文等。這些狀態可能需要額外的內存和處理開銷,並增加運行時的複雜性。同時,執行異步操作可能需要調度和管理協程或線程的運行,涉及到上下文切換和調度器的開銷。
然後我們感謝 C 艹 老鐵送來的 LLVM 的 coroutine, LLVM 的 Coroutine 插件通過轉換代碼、生成狀態機以及對協程切換點進行優化,以實現高效的協程執行。它針對協程切換點的位置進行標記,並利用生成的狀態機來控制協程的切換。這樣可以避免不必要的上下文切換,減少性能開銷和內存佔用。
總結#
Go 偷懶了,技術迭代了,Go 好像又贏了