forked from eddycjy/blog
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
352 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,352 @@ | ||
# 深入理解 Go defer | ||
|
||
在上一章节 [《深入理解 Go panic and recover》](https://github.com/EDDYCJY/blog/blob/master/golang/pkg/2019-05-18-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Go-panic-and-recover.md) 中,我们发现了 `defer` 与其关联性极大,还是觉得非常有必要深入一下。希望通过本章节大家可以对 `defer` 关键字有一个深刻的理解,那么我们开始吧。你先等等,请排好队,我们这儿采取后进先出 LIFO 的出站方式... | ||
|
||
## 特性 | ||
|
||
我们简单的过一下 `defer` 关键字的基础使用,让大家先有一个基础的认知 | ||
|
||
### 一、延迟调用 | ||
|
||
``` | ||
func main() { | ||
defer log.Println("EDDYCJY.") | ||
log.Println("end.") | ||
} | ||
``` | ||
|
||
输出结果: | ||
|
||
``` | ||
$ go run main.go | ||
2019/05/19 21:15:02 end. | ||
2019/05/19 21:15:02 EDDYCJY. | ||
``` | ||
|
||
### 二、后进先出 | ||
|
||
``` | ||
func main() { | ||
for i := 0; i < 6; i++ { | ||
defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".") | ||
} | ||
log.Println("end.") | ||
} | ||
``` | ||
|
||
输出结果: | ||
|
||
``` | ||
$ go run main.go | ||
2019/05/19 21:19:17 end. | ||
2019/05/19 21:19:17 EDDYCJY5. | ||
2019/05/19 21:19:17 EDDYCJY4. | ||
2019/05/19 21:19:17 EDDYCJY3. | ||
2019/05/19 21:19:17 EDDYCJY2. | ||
2019/05/19 21:19:17 EDDYCJY1. | ||
2019/05/19 21:19:17 EDDYCJY0. | ||
``` | ||
|
||
### 三、运行时间点 | ||
|
||
``` | ||
func main() { | ||
func() { | ||
defer log.Println("defer.EDDYCJY.") | ||
}() | ||
log.Println("main.EDDYCJY.") | ||
} | ||
``` | ||
|
||
输出结果: | ||
|
||
``` | ||
$ go run main.go | ||
2019/05/22 23:30:27 defer.EDDYCJY. | ||
2019/05/22 23:30:27 main.EDDYCJY. | ||
``` | ||
|
||
### 四、异常处理 | ||
|
||
``` | ||
func main() { | ||
defer func() { | ||
if e := recover(); e != nil { | ||
log.Println("EDDYCJY.") | ||
} | ||
}() | ||
panic("end.") | ||
} | ||
``` | ||
|
||
输出结果: | ||
|
||
``` | ||
$ go run main.go | ||
2019/05/20 22:22:57 EDDYCJY. | ||
``` | ||
|
||
## 源码剖析 | ||
|
||
``` | ||
$ go tool compile -S main.go | ||
"".main STEXT size=163 args=0x0 locals=0x40 | ||
... | ||
0x0059 00089 (main.go:6) MOVQ AX, 16(SP) | ||
0x005e 00094 (main.go:6) MOVQ $1, 24(SP) | ||
0x0067 00103 (main.go:6) MOVQ $1, 32(SP) | ||
0x0070 00112 (main.go:6) CALL runtime.deferproc(SB) | ||
0x0075 00117 (main.go:6) TESTL AX, AX | ||
0x0077 00119 (main.go:6) JNE 137 | ||
0x0079 00121 (main.go:7) XCHGL AX, AX | ||
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) | ||
0x007f 00127 (main.go:7) MOVQ 56(SP), BP | ||
0x0084 00132 (main.go:7) ADDQ $64, SP | ||
0x0088 00136 (main.go:7) RET | ||
0x0089 00137 (main.go:6) XCHGL AX, AX | ||
0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB) | ||
0x008f 00143 (main.go:6) MOVQ 56(SP), BP | ||
0x0094 00148 (main.go:6) ADDQ $64, SP | ||
0x0098 00152 (main.go:6) RET | ||
... | ||
``` | ||
|
||
首先我们需要找到它,找到它实际对应什么执行代码。通过汇编代码,可得知涉及如下方法: | ||
|
||
- runtime.deferproc | ||
- runtime.deferreturn | ||
|
||
很显然是运行时的方法,是对的人。我们继续往下走看看都分别承担了什么行为 | ||
|
||
### 数据结构 | ||
|
||
在开始前我们需要先介绍一下 `defer` 的基础单元 `_defer` 结构体,如下: | ||
|
||
``` | ||
type _defer struct { | ||
siz int32 | ||
started bool | ||
sp uintptr // sp at time of defer | ||
pc uintptr | ||
fn *funcval | ||
_panic *_panic // panic that is running defer | ||
link *_defer | ||
} | ||
... | ||
type funcval struct { | ||
fn uintptr | ||
// variable-size, fn-specific data here | ||
} | ||
``` | ||
|
||
- siz:所有传入参数的总大小 | ||
- started:该 `defer` 是否已经执行过 | ||
- sp:函数栈指针寄存器,一般指向当前函数栈的栈顶 | ||
- pc:程序计数器,有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC指向的是下一条指令,而不是当前指令 | ||
- fn:指向传入的函数地址和参数 | ||
- _panic:指向 `_panic` 链表 | ||
- link:指向 `_defer` 链表 | ||
|
||
![image](https://i.imgur.com/f2KghN9.png) | ||
|
||
### deferproc | ||
|
||
``` | ||
func deferproc(siz int32, fn *funcval) { | ||
... | ||
sp := getcallersp() | ||
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) | ||
callerpc := getcallerpc() | ||
d := newdefer(siz) | ||
... | ||
d.fn = fn | ||
d.pc = callerpc | ||
d.sp = sp | ||
switch siz { | ||
case 0: | ||
// Do nothing. | ||
case sys.PtrSize: | ||
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) | ||
default: | ||
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) | ||
} | ||
return0() | ||
} | ||
``` | ||
|
||
- 获取调用 `defer` 函数的函数栈指针、传入函数的参数具体地址以及PC (程序计数器),也就是下一个要执行的指令。这些相当于是预备参数,便于后续的流转控制 | ||
- 创建一个新的 `defer` 最小单元 `_defer`,填入先前准备的参数 | ||
- 调用 `memmove` 将传入的参数存储到新 `_defer` (当前使用)中去,便于后续的使用 | ||
- 最后调用 `return0` 进行返回,这个函数非常重要。能够避免在 `deferproc` 中又因为返回 `return`,而诱发 `deferreturn` 方法的调用。其根本原因是一个停止 `panic` 的延迟方法会使 `deferproc` 返回 1,但在机制中如果 `deferproc` 返回不等于 0,将会总是检查返回值并跳转到函数的末尾。而 `return0` 返回的就是 0,因此可以防止重复调用 | ||
|
||
#### 小结 | ||
|
||
在**这个函数中会为新的 `_defer` 设置一些基础属性,并将调用函数的参数集传入。最后通过特殊的返回方法结束函数调用**。另外这一块与先前 [《深入理解 Go panic and recover》](https://segmentfault.com/a/1190000019251478#articleHeader9) 的处理逻辑有一定关联性,其实就是 `gp.sched.ret` 返回 0 还是 1 会分流至不同处理方式 | ||
|
||
### newdefer | ||
|
||
``` | ||
func newdefer(siz int32) *_defer { | ||
var d *_defer | ||
sc := deferclass(uintptr(siz)) | ||
gp := getg() | ||
if sc < uintptr(len(p{}.deferpool)) { | ||
pp := gp.m.p.ptr() | ||
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil { | ||
... | ||
lock(&sched.deferlock) | ||
d := sched.deferpool[sc] | ||
unlock(&sched.deferlock) | ||
} | ||
... | ||
} | ||
if d == nil { | ||
systemstack(func() { | ||
total := roundupsize(totaldefersize(uintptr(siz))) | ||
d = (*_defer)(mallocgc(total, deferType, true)) | ||
}) | ||
... | ||
} | ||
d.siz = siz | ||
d.link = gp._defer | ||
gp._defer = d | ||
return d | ||
} | ||
``` | ||
|
||
- 从池中获取可以使用的 `_defer`,则复用作为新的基础单元 | ||
- 若在池中没有获取到可用的,则调用 `mallocgc` 重新申请一个新的 | ||
- 设置 `defer` 的基础属性,最后修改当前 `Goroutine` 的 `_defer` 指向 | ||
|
||
通过这个方法我们可以注意到两点,如下: | ||
|
||
- `defer` 与 `Goroutine(g)` 有直接关系,所以讨论 `defer` 时基本离不开 `g` 的关联 | ||
- 新的 `defer` 总是会在现有的链表中的最前面,也就是 `defer` 的特性后进先出 | ||
|
||
#### 小结 | ||
|
||
这个函数主要承担了获取新的 `_defer` 的作用,它有可能是从 `deferpool` 中获取的,也有可能是重新申请的 | ||
|
||
### deferreturn | ||
|
||
``` | ||
func deferreturn(arg0 uintptr) { | ||
gp := getg() | ||
d := gp._defer | ||
if d == nil { | ||
return | ||
} | ||
sp := getcallersp() | ||
if d.sp != sp { | ||
return | ||
} | ||
switch d.siz { | ||
case 0: | ||
// Do nothing. | ||
case sys.PtrSize: | ||
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d)) | ||
default: | ||
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz)) | ||
} | ||
fn := d.fn | ||
d.fn = nil | ||
gp._defer = d.link | ||
freedefer(d) | ||
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) | ||
} | ||
``` | ||
|
||
如果在一个方法中调用过 `defer` 关键字,那么编译器将会在结尾处插入 `deferreturn` 方法的调用。而该方法中主要做了如下事项: | ||
|
||
- 清空当前节点 `_defer` 被调用的函数调用信息 | ||
- 释放当前节点的 `_defer` 的存储信息并放回池中(便于复用) | ||
- 跳转到调用 `defer` 关键字的调用函数处 | ||
|
||
在这段代码中,跳转方法 `jmpdefer` 格外重要。因为它显式的控制了流转,代码如下: | ||
|
||
``` | ||
// asm_amd64.s | ||
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16 | ||
MOVQ fv+0(FP), DX // fn | ||
MOVQ argp+8(FP), BX // caller sp | ||
LEAQ -8(BX), SP // caller sp after CALL | ||
MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use) | ||
SUBQ $5, (SP) // return to CALL again | ||
MOVQ 0(DX), BX | ||
JMP BX // but first run the deferred function | ||
``` | ||
|
||
通过源码的分析,我们发现它做了两个很 “奇怪” 又很重要的事,如下: | ||
|
||
- MOVQ -8(SP), BP:`-8(BX)` 这个位置保存的是 `deferreturn` 执行完毕后的地址 | ||
- SUBQ $5, (SP):`SP` 的地址减 5 ,其减掉的长度就恰好是 `runtime.deferreturn` 的长度 | ||
|
||
你可能会问,为什么是 5?好吧。翻了半天最后看了一下汇编代码...嗯,相减的确是 5 没毛病,如下: | ||
|
||
``` | ||
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) | ||
0x007f 00127 (main.go:7) MOVQ 56(SP), BP | ||
``` | ||
|
||
我们整理一下思绪,照上述逻辑的话,那 `deferreturn` 就是一个 “递归” 了哦。每次都会重新回到 `deferreturn` 函数,那它在什么时候才会结束呢,如下: | ||
|
||
``` | ||
func deferreturn(arg0 uintptr) { | ||
gp := getg() | ||
d := gp._defer | ||
if d == nil { | ||
return | ||
} | ||
... | ||
} | ||
``` | ||
|
||
也就是会不断地进入 `deferreturn` 函数,判断链表中是否还存着 `_defer`。若已经不存在了,则返回,结束掉它。简单来讲,就是处理完全部 `defer` 才允许你真的离开它。果真如此吗?我们再看看上面的汇编代码,如下: | ||
|
||
``` | ||
。.. | ||
0x0070 00112 (main.go:6) CALL runtime.deferproc(SB) | ||
0x0075 00117 (main.go:6) TESTL AX, AX | ||
0x0077 00119 (main.go:6) JNE 137 | ||
0x0079 00121 (main.go:7) XCHGL AX, AX | ||
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) | ||
0x007f 00127 (main.go:7) MOVQ 56(SP), BP | ||
0x0084 00132 (main.go:7) ADDQ $64, SP | ||
0x0088 00136 (main.go:7) RET | ||
0x0089 00137 (main.go:6) XCHGL AX, AX | ||
0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB) | ||
... | ||
``` | ||
|
||
的确如上述流程所分析一致,验证完毕 | ||
|
||
#### 小结 | ||
|
||
这个函数主要承担了清空已使用的 `defer` 和跳转到调用 `defer` 关键字的函数处,非常重要 | ||
|
||
## 总结 | ||
|
||
我们有提到 `defer` 关键字涉及两个核心的函数,分别是 `deferproc` 和 `deferreturn` 函数。而 `deferreturn` 函数比较特殊,是当应用函数调用 `defer` 关键字时,编译器会在其结尾处插入 `deferreturn` 的调用,它们俩一般都是成对出现的 | ||
|
||
但是当一个 `Goroutine` 上存在着多次 `defer` 行为(也就是多个 `_defer`)时,编译器会进行利用一些小技巧, 重新回到 `deferreturn` 函数去消耗 `_defer` 链表,直到一个不剩才允许真正的结束 | ||
|
||
而新增的基础单元 `_defer`,有可能是被复用的,也有可能是全新申请的。它最后都会被追加到 `_defer` 链表的表头,从而设定了后进先出的调用特性 | ||
|
||
## 关联 | ||
|
||
- [深入理解 Go panic and recover](https://github.com/EDDYCJY/blog/blob/master/golang/pkg/2019-05-18-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Go-panic-and-recover.md) | ||
|
||
## 参考 | ||
|
||
- [Scheduling In Go](https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html) | ||
- [Dive into stack and defer/panic/recover in go](http://hustcat.github.io/dive-into-stack-defer-panic-recover-in-go/) | ||
- [golang-notes](https://github.com/cch123/golang-notes/blob/master/defer.md) |