【译文】原文地址
本文是基于Go 1.14版本
在Go中,goroutine只不过是一个包含正在运行的程序信息的go结构体,例如栈、程序计数器和当前操作系统线程。Go调度器处理这些信息,为它们提供运行时间。调度器还必须负责goroutine的启动和退出,这两个阶段必须谨慎的管理。
关于堆栈和程序计数器的更多内容,建议阅读Goroutine切换实际包含哪些
Go启动
启动一个goroutine十分简单,以下一段程序为例:
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
println("goroutine is running...")
wg.Done()
}()
println("main is running")
wg.Wait()
}
main函数在打印一条消息之前启动一个goroutine。因为goroutine有自己的运行时,Go通知运行时建立一个新的goroutine,具体执行:
- 创建栈
- 收集关于当前程序计数器或者调用者数据信息。
- 更新goroutine的内部数据,例如ID或状态。
然而,goroutine并不会立即获得执行。新创建的goroutine将在本地队列开始处进入队列,并在Go调度器的下一轮运行。如下是一个当前运行状态:将goroutine放在队列的头,使其成为当前goroutine之后第一个运行的goroutine。它将运行在当前线程或另一个线程中,如果发生了抢占调度。
关于更多goroutine抢占调度内容,可参考Go调度器的抢占调度
Goroutine的创建也可以在汇编指令当中看到:一旦goroutine被创建并push到本地调度队列当中,程序将直接进入main函数的下一条指令。
Goroutine的退出
当一个goroutine执行结束时,Go必须调度其他的goroutine,避免浪费CPU时间。将会保留goroutine以便后面重用。
可以阅读更多关于goroutine回收内容:Go: How Does Go Recycle Goroutines?
然而,Go需要一种方法来感知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 skip int
for {
_, file, line, ok := runtime.Caller(skip)
if !ok{
break
}
fmt.Printf("%s:%d\n", file, line)
skip++
}
wg.Done()
}()
wg.Wait()
}
output:
C:/Users/Administrator/go/src/vault/main.go:16
C:/Go/src/runtime/asm_amd64.s:1357
以汇编方式编写的文件asm_amd64包含如下函数:
然后Go将切换到g0来调度其他的goroutine。也可以通过runtime.Goexit()函数来手动终止goroutine的执行。
package main
import (
"runtime"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
runtime.Goexit()// goroutine exits here
println("never executed")
}()
wg.Wait()
}
这个函数将首先执行defer函数,然后在goroutine退出前调用goexit函数。