探索goroutine的销毁


go version = 1.9.3

GOARCH=“amd64” GOOS=“darwin”

本文使用delve进行调试

goroutine实际不会被销毁,而是结束后放在free goroutine里等待被再次使用

测试工程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// $GOPATH/test/main.go
package main

import (
	"fmt"
)

var ch = make(chan bool)

func hello() {
	fmt.Println("hello world")
	close(ch)
}

func main() {
	go hello()
	<- ch
}

从goroutine运行时的堆栈找下线索

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ go build main.go
$ test dlv exec main
Type 'help' for list of commands.
(dlv) b main.hello
Breakpoint 1 set at 0x10933f3 for main.hello() ./main.go:9
(dlv) c
> main.hello() ./main.go:9 (hits goroutine(5):1 total:1) (PC: 0x10933f3)
Warning: debugging optimized function
     4:		"fmt"
     5:	)
     6:
     7:	var ch = make(chan bool)
     8:
=>   9:	func hello() {
    10:		fmt.Println("hello world")
    11:		close(ch)
    12:	}
    13:
    14:	func main() {
(dlv) n
# ....
(dlv) bt
0  0x0000000001093401 in main.hello
   at ./main.go:10
1  0x00000000010506d1 in runtime.goexit
   at /usr/local/go/src/runtime/asm_amd64.s:2337
(dlv)

runtime.goexit()

每个goroutine栈底都会有runtime.goexit(),它其实就是在创建G的时候,被设置进去的。

1
2
3
4
5
6
7
8
9
// Create a new g running fn with narg bytes of arguments starting
// at argp and returning nret bytes of results.  callerpc is the
// address of the go statement that created this. The new g is put
// on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
    // 省略部分代码
	newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    // 省略部分代码
}

如果只关心已经执行完毕的goroutine是如何处置的,跟踪源码发现实质起干活的是goexit0()。

1
2
3
4
5
6
// src/runtime/asm_amd64.s
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/runtime/proc.go
func goexit1() {
	if raceenabled {
		racegoend()
	}
	if trace.enabled {
		traceGoEnd()
	}
	mcall(goexit0)
}

mcall()是使用go汇编语言实现的。它的参数是一个函数类型。func mcall(fn func(*g))

作用就是将当前线程的执行栈先切换到g0上。然后将当前的goroutine(即执行完的goroutine对应的G实例)当成参数传给fn,在g0的栈上继续执行fn。等于换了个SP指针,然后继续执行goexit0。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// func mcall(fn func(*g))
// Switch to m->g0's stack, call fn(g).
// Fn must never return. It should gogo(&g->sched)
// to keep running g.
TEXT runtime·mcall(SB), NOSPLIT, $0-8
    // 把待执行的函数放进DI寄存器
	MOVQ	fn+0(FP), DI

	// 这里应该是拿到g
	get_tls(CX)
	MOVQ	g(CX), AX	// save state in g->sched
	MOVQ	0(SP), BX	// caller's PC
	// 把g的现场寄存器信息存到g.sched里
	MOVQ	BX, (g_sched+gobuf_pc)(AX)
	LEAQ	fn+0(FP), BX	// caller's SP
	MOVQ	BX, (g_sched+gobuf_sp)(AX)
	MOVQ	AX, (g_sched+gobuf_g)(AX)
	MOVQ	BP, (g_sched+gobuf_bp)(AX)

	// switch to m->g0 & its stack, call fn
	// 找出g0
	MOVQ	g(CX), BX
	MOVQ	g_m(BX), BX
	MOVQ	m_g0(BX), SI
	// 此时 SI = g0, AX = g
	CMPQ	SI, AX	// if g == m->g0 call badmcall
	JNE	3(PC)
	// 如果当前的g就是g0,就执行badmcall,否则跳过下面的三条指令
	MOVQ	$runtime·badmcall(SB), AX
	JMP	AX
	MOVQ	SI, g(CX)	// g = m->g0
	// 切到g0的栈上
	MOVQ	(g_sched+gobuf_sp)(SI), SP	// sp = m->g0->sched.sp
	// 推g进栈
	PUSHQ	AX
	MOVQ	DI, DX
	MOVQ	0(DI), DI
	// 调用fn,并且永远都不应该返回,否则执行badmcall2
	CALL	DI
	POPQ	AX
	MOVQ	$runtime·badmcall2(SB), AX
	JMP	AX
	RET

runtime.goexit0()

干了几件事:

  1. G的状态变为_GDead,如果是系统G则更新全局计数器。
  2. 重置G身上一系列的属性变量。
  3. 解除M和G的互相引用关系。
  4. 放置在本地P或全局的free goroutine队列。
  5. 调度,寻找下一个可运行的goroutine。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// src/runtime/proc.go
// goexit continuation on g0.
func goexit0(gp *g) {
	// 这里取到g0
	_g_ := getg()

	// 将已经执行完毕的goroutine状态设为dead
	casgstatus(gp, _Grunning, _Gdead)
	// 如果是系统goroutine则全局计数减一
	if isSystemGoroutine(gp) {
		atomic.Xadd(&sched.ngsys, -1)
	}

	// 清空
	gp.m = nil
	gp.lockedm = nil
	// lockedg什么作用?
	_g_.m.lockedg = nil
	gp.paniconfault = false
	gp._defer = nil // should be true already but just in case.
	gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
	gp.writebuf = nil
	gp.waitreason = ""
	gp.param = nil
	gp.labels = nil
	gp.timer = nil

	// Note that gp's stack scan is now "valid" because it has no
	// stack.
	gp.gcscanvalid = true
	// 因为M和正在运行的G是有互相引用的,G都已经执行完了,所以就摘掉
	dropg()

	if _g_.m.locked&^_LockExternal != 0 {
		print("invalid m->locked = ", _g_.m.locked, "\n")
		throw("internal lockOSThread error")
	}
	_g_.m.locked = 0
	// 把已经执行完的goroutine放置在P的本地free goroutine队列里,最多64个,超出则转移到全局调度器那儿
	gfput(_g_.m.p.ptr(), gp)
	// 调度,找出下一个可以执行的goroutine,继续执行
	schedule()
}