iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0

Goroutine

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 有多輕量?

這本書試著用這個例子來呈現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

上一篇
5.CSP & Go
下一篇
7.Sync package
系列文
Concurrency in go 讀書心得30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言