写在前面
本文基于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
", 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退出时,将调用之前看到的同一个函数。