goroutine 是 Go 語言的輕量級執行緒。當你使用 go 關鍵字啟動一個函數時,該函數將在一個新的 goroutine 中非同步地運行,也就是該函數的執行與調用它的函數是併發地運行的。
來看兩個簡單的例子
func main() {
salutation := "hello"
go func() {
defer wg.Done()
salutation = "welcome" // <1>
}()
fmt.Println(salutation)
}
go func() {...}() 會啟動一個新的 goroutine,該 goroutine 將試圖修改 salutation 的值。
由於 main 函式不會等待該 goroutine 完成,因此 fmt.Println(salutation) 可能在 goroutine 修改 salutation 之前或之後運行,輸出可能是 "hello" 或 "welcome"。這表示你會看到不確定的行為。
func main() {
var wg sync.WaitGroup
salutation := "hello"
wg.Add(1)
go func() {
defer wg.Done()
salutation = "welcome" // <1>
}()
wg.Wait()
fmt.Println(salutation)
}
這個例子使用 sync.WaitGroup 確保 main 函式等待 goroutine 完成。
由於 wg.Wait(),主函式會等待 goroutine 完成其工作,這意味著當我們到達 fmt.Println(salutation) 時,salutation 一定已經被修改為 "welcome",所以輸出將始終是 "welcome"。
再來看看這個程式碼
func main() {
var wg sync.WaitGroup
for _, salutation := range []string{"hello", "greetings", "good day"} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(salutation) // <1>
}()
}
wg.Wait()
}
你覺得會印出什麼呢?
如果你覺得是
hello
greetings
good day
那你就錯了。
Go在迴圈中使用匿名函數或 goroutines 時,捕獲了迴圈變數的引用,而不是當前的值。
是因為迴圈變數在迴圈迭代的每一步中都被重新賦值,而且所有迭代使用的都是同一個變數的記憶體位置。
例如
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f()
}
你可能希望這段程式碼的輸出是:
0
1
2
但事實上,當你運行這段程式碼時,輸出將是:
3
3
3
因為匿名函數捕獲了變數 i 的引用,而不是在每次迭代時 i 的當前值。當迴圈結束後,i 的值為 3,所以當我們調用每一個匿名函數時,它們都打印出 3。
同樣的情況也適用於 goroutines。當你在迴圈中啟動 goroutines 並捕獲迴圈變數時,你可能會遇到這個問題。
為了解決這個問題,一個常見的做法是將迴圈變數作為參數傳遞給匿名函數或 goroutine,這樣你就會捕獲迴圈變數的當前值,而不是其引用。
例如
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func(j int) func() {
return func() {
fmt.Println(j)
}
}(i))
}
for _, f := range funcs {
f()
}
所以回過頭來,要解決原本的問題,就是把原本的程式改成這樣
func main() {
var wg sync.WaitGroup
for _, salutation := range []string{"hello", "greetings", "good day"} {
wg.Add(1)
go func(salutation string) { // <1>
defer wg.Done()
fmt.Println(salutation)
}(salutation) // <2>
}
wg.Wait()
}
這本書試著用這個例子來呈現goroutine的資源使用量
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
// 這個函數返回當前消耗的系統內存量
memConsumed := func() uint64 {
runtime.GC() // 強制運行垃圾回收,以獲得更精確的內存使用情況
var s runtime.MemStats
runtime.ReadMemStats(&s) // 讀取當前的內存統計
return s.Sys
}
var c <-chan interface{}
var wg sync.WaitGroup
noop := func() {
wg.Done() // 通知WaitGroup一個操作已完成
<-c // 這將阻止goroutine,因為這個channel永遠不會發送數據或關閉
} // 定義一個不做任何事情的匿名函數,該函數會陷入無限等待,因為它試圖從一個永遠不會接收到任何數據的通道讀取數據。
const numGoroutines = 1e4 // 定義要創建的goroutines的數量 <2>
wg.Add(numGoroutines) // 設置WaitGroup的數量
before := memConsumed() // 測量創建goroutines之前的內存使用量 <3>
for i := numGoroutines; i > 0; i-- {
go noop() // 啟動goroutines
}
wg.Wait() // 等待所有goroutines報告它們已完成
after := memConsumed() // 創建了所有的 goroutines 之後,再次使用 memConsumed 函數測量內存使用情況。然後計算差值,以估算每個 goroutine 的平均內存使用量。
fmt.Printf("%.3fkb", float64(after-before)/numGoroutines/1000) // 輸出每個goroutine平均使用的內存大小
}
我的本機是3.047kb