goroutine是如何啟停的
寫在前面
本文基於GoLang 1.14
在Go中,goroutine不過是一個Go結構,其中包含了關於正在執行的程式的資訊,如堆疊、程式計數器或其當前的作業系統執行緒。Go排程器會處理這些資訊,給它們提供執行時間。排程器在goroutine的啟動和退出時也要注意,這兩個階段需要小心管理。
啟動
建立一個goroutine非常的簡單,如下:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(1) go func() { fmt.Println("Hello") wg.Done() }() fmt.Println("Done") wg.Wait() }
主函式在列印訊息之前,會啟動一個goroutine。由於goroutine會有自己的執行時間,Go會通知執行時建立一個新的goroutine,也就是說:
- 建立堆疊。
- 收集當前程式計數器或呼叫者的資料資訊。
- 更新goroutine的內部資料,如ID或狀態。
但是,goroutine不會立即獲得任何執行時間。新建立的goroutine將被放到在本地佇列的開頭,並在下一輪GoLang排程器中執行。
把goroutine放在佇列的頭,目的是使它在當前goroutine之後第一個執行。如果有work-stealing的情況發生,它將在同一執行緒或另一執行緒上執行。
我們可以從下面的彙編程式碼中看到goroutine的建立:
一旦goroutine被建立並推送到本地的goroutine佇列中,它就直接進入主函式的下一條指令。
退出
當一個goroutine結束時,為了不浪費CPU時間,Go必須排程另一個goroutine。同時,它還會保留當前這個goroutine,以便以後重複使用。
然而,GoLang需要一種方法來感知goroutine的結束。這個控制是在建立goroutine的過程中。
在建立goroutine時,Go在將程式計數器設定為goroutine呼叫的真正函式之前,會將棧設定為一個名為goexit
的函式,這樣可以保證goroutine在結束工作後呼叫函式goexit
。
關於上面的描述,我們通過以下程式碼進行展示一下:
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
var skInt int
for {
_, file, line, ok := runtime.Caller(skInt)
if !ok {
break
}
fmt.Printf("%s:%d\n", file, line)
skInt++
}
wg.Done()
}()
fmt.Println("Done")
wg.Wait()
}
執行程式碼得到如下輸出:
F:\hello>go run main.go
Done
F:/hello/main.go:16
D:/Go/src/runtime/asm_amd64.s:1374
從asm_amd64.s
這個檔案中,我們可以看到有如下的函式的定義:
// The top-most function running on a goroutine
// returns to goexit+PCQuantum.
TEXT runtime·goexit(SB),NOSPLIT,$0-0
BYTE $0x90 // NOP
CALL runtime·goexit1(SB) // does not return
// traceback from goexit1 must hit code range of goexit
BYTE $0x90 // NOP
我們可以看到,GoLang將會切換到g0
這個goroutine從而去排程其它的goroutine。
在我們程式碼中,我們也可以呼叫runtime.Goexit()
去主動退出。
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
runtime.Goexit()
fmt.Println("exist")
}()
wg.Wait()
}
這個函式將首先執行defer
函式,然後當一個goroutine退出時,將呼叫之前看到的同一個函式。