0%

《Golang》Select 原理

要分析 select 原理,首先看看 select 会被编译器如何处理,编译下面代码

1
2
3
4
5
6
7
8
9
10
11
package main

func main() {
testChan := make(chan bool, 0)

select {
case <- testChan:
default:

}
}

编译

1
GOARCH=amd64 go build -gcflags=all="-N -l" ./cmd/main/   # -N 禁止编译器优化,-l 禁止内联编译

打印汇编

1
go tool objdump -s "main.main" main
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
TEXT main.main(SB) ./cmd/main/main.go
main.go:3 0x1067ca0 65488b0c2530000000 MOVQ GS:0x30, CX
main.go:3 0x1067ca9 483b6110 CMPQ 0x10(CX), SP
main.go:3 0x1067cad 7663 JBE 0x1067d12
main.go:3 0x1067caf 4883ec30 SUBQ $0x30, SP # 栈顶下移
main.go:3 0x1067cb3 48896c2428 MOVQ BP, 0x28(SP) # 备份栈基
main.go:3 0x1067cb8 488d6c2428 LEAQ 0x28(SP), BP # 设置栈基,形成栈基到栈顶之间的栈帧。这里往上都是 main 函数分配栈帧
main.go:4 0x1067cbd 488d053c6e0000 LEAQ runtime.types+27904(SB), AX
main.go:4 0x1067cc4 48890424 MOVQ AX, 0(SP)
main.go:4 0x1067cc8 48c744240800000000 MOVQ $0x0, 0x8(SP)
main.go:4 0x1067cd1 e80ac0f9ff CALL runtime.makechan(SB)
main.go:4 0x1067cd6 488b442410 MOVQ 0x10(SP), AX
main.go:4 0x1067cdb 4889442418 MOVQ AX, 0x18(SP) # 这里往上都是 testChan 变量的初始化
main.go:7 0x1067ce0 4889442420 MOVQ AX, 0x20(SP)
main.go:7 0x1067ce5 48c7042400000000 MOVQ $0x0, 0(SP)
main.go:7 0x1067ced 4889442408 MOVQ AX, 0x8(SP)
main.go:7 0x1067cf2 e8a9d6f9ff CALL runtime.selectnbrecv(SB) # 尝试接收数据,selectnbrecv 接收不到就返回 false
main.go:7 0x1067cf7 807c241000 CMPB $0x0, 0x10(SP) # selectnbrecv 的执行结果与 0 做比较
main.go:7 0x1067cfc 7504 JNE 0x1067d02 # 不等于 false ,也就是取到数据了,则跳转到 0x1067d02 执行 case 分支中的代码
main.go:7 0x1067cfe 6690 NOPW
main.go:7 0x1067d00 eb0e JMP 0x1067d10 # 这里就是跳到 default 分支执行其中代码
main.go:7 0x1067d02 eb00 JMP 0x1067d04 # 因为分支中没有任何代码,所以接着跳,跳出 select
main.go:7 0x1067d04 eb00 JMP 0x1067d06 # 接着跳,跳出 select
main.go:7 0x1067d06 488b6c2428 MOVQ 0x28(SP), BP # 恢复栈基
main.go:7 0x1067d0b 4883c430 ADDQ $0x30, SP # 清除 main 栈帧
main.go:7 0x1067d0f c3 RET # 返回 main 的调用者
main.go:7 0x1067d10 ebf2 JMP 0x1067d04 # 因为 default 分支中没有任何代码,所以接着跳,跳出 select
main.go:3 0x1067d12 e809b3ffff CALL runtime.morestack_noctxt(SB)
main.go:3 0x1067d17 eb87 JMP main.main(SB)

从上面的汇编分析可以看出,select 被编译成了先执行 case 中的条件,条件满足则执行其中代码,否则执行 default 分支中的代码

可以看出 default 的在 case 条件不满足的情况下才执行的

那么,如果存在多个 case 语句的,select 该如何抉择,接着看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
testChan := make(chan bool, 0)
testChan1 := make(chan uint64, 0)

select {
case <- testChan:
fmt.Println(1)
case <- testChan1:
fmt.Println(2)
default:
fmt.Println(3)
}
}

同样查看汇编代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
TEXT main.main(SB) ./cmd/main/main.go
main.go:5 0x10cd2c0 65488b0c2530000000 MOVQ GS:0x30, CX
main.go:5 0x10cd2c9 488d842460ffffff LEAQ 0xffffff60(SP), AX
main.go:5 0x10cd2d1 483b4110 CMPQ 0x10(CX), AX
main.go:5 0x10cd2d5 0f86a5020000 JBE 0x10cd580
main.go:5 0x10cd2db 4881ec20010000 SUBQ $0x120, SP
main.go:5 0x10cd2e2 4889ac2418010000 MOVQ BP, 0x118(SP)
main.go:5 0x10cd2ea 488dac2418010000 LEAQ 0x118(SP), BP
main.go:6 0x10cd2f2 488d0587a90000 LEAQ runtime.rodata+42432(SB), AX
main.go:6 0x10cd2f9 48890424 MOVQ AX, 0(SP)
main.go:6 0x10cd2fd 48c744240800000000 MOVQ $0x0, 0x8(SP)
main.go:6 0x10cd306 e89578f3ff CALL runtime.makechan(SB)
main.go:6 0x10cd30b 488b442410 MOVQ 0x10(SP), AX
main.go:6 0x10cd310 4889442460 MOVQ AX, 0x60(SP) # 这里往上是 testChan 的初始化
main.go:7 0x10cd315 488d05e4a90000 LEAQ runtime.rodata+42560(SB), AX
main.go:7 0x10cd31c 48890424 MOVQ AX, 0(SP)
main.go:7 0x10cd320 48c744240800000000 MOVQ $0x0, 0x8(SP)
main.go:7 0x10cd329 e87278f3ff CALL runtime.makechan(SB)
main.go:7 0x10cd32e 488b442410 MOVQ 0x10(SP), AX
main.go:7 0x10cd333 4889442458 MOVQ AX, 0x58(SP) # 这里往上是 testChan1 的初始化
main.go:10 0x10cd338 488b442460 MOVQ 0x60(SP), AX # testChan 的值取出来放到 AX
main.go:10 0x10cd33d 4889442478 MOVQ AX, 0x78(SP) # testChan 的值存到 0x78(SP) 处
main.go:12 0x10cd342 488b442458 MOVQ 0x58(SP), AX # testChan1 的值取出来放到 AX
main.go:12 0x10cd347 4889442470 MOVQ AX, 0x70(SP) # testChan1 的值存到 0x70(SP) 处
main.go:9 0x10cd34c 0f57c0 XORPS X0, X0
main.go:9 0x10cd34f 0f118424f8000000 MOVUPS X0, 0xf8(SP)
main.go:9 0x10cd357 0f11842408010000 MOVUPS X0, 0x108(SP)
main.go:10 0x10cd35f 488b442478 MOVQ 0x78(SP), AX
main.go:10 0x10cd364 4889842408010000 MOVQ AX, 0x108(SP) # 又复制了一波 testChan
main.go:12 0x10cd36c 488b442470 MOVQ 0x70(SP), AX
main.go:12 0x10cd371 48898424f8000000 MOVQ AX, 0xf8(SP) # 又复制了一波 testChan1
main.go:9 0x10cd379 488d8424f8000000 LEAQ 0xf8(SP), AX
main.go:9 0x10cd381 4889442468 MOVQ AX, 0x68(SP)
main.go:9 0x10cd386 488d442450 LEAQ 0x50(SP), AX
main.go:9 0x10cd38b 4889842498000000 MOVQ AX, 0x98(SP)
main.go:9 0x10cd393 488b4c2468 MOVQ 0x68(SP), CX
main.go:9 0x10cd398 48890c24 MOVQ CX, 0(SP)
main.go:9 0x10cd39c 4889442408 MOVQ AX, 0x8(SP)
main.go:9 0x10cd3a1 0f57c0 XORPS X0, X0
main.go:9 0x10cd3a4 0f11442410 MOVUPS X0, 0x10(SP) # 第一个参数 cas0
main.go:9 0x10cd3a9 48c744242002000000 MOVQ $0x2, 0x20(SP) # 第二个参数 order0
main.go:9 0x10cd3b2 c644242800 MOVB $0x0, 0x28(SP) # 第三个参数 ncases
main.go:9 0x10cd3b7 e884edf7ff CALL runtime.selectgo(SB) # 调用 selectgo 选择 case
main.go:9 0x10cd3bc 488b442430 MOVQ 0x30(SP), AX # selectgo 的第一个返回值,是选择了哪个 case ,0 代表 testChan1,1 代表 testChan
main.go:9 0x10cd3c1 0fb64c2438 MOVZX 0x38(SP), CX # selectgo 的第二个返回值,表示是否能从 chan 接收成功,这个返回值没啥用
main.go:9 0x10cd3c6 4889442448 MOVQ AX, 0x48(SP) # 暂存第一个返回值内容
main.go:9 0x10cd3cb 884c2447 MOVB CL, 0x47(SP) # 暂存第二个返回值内容
main.go:14 0x10cd3cf 48837c244800 CMPQ $0x0, 0x48(SP) # 第一个返回值与 0 比较
main.go:14 0x10cd3d5 7c05 JL 0x10cd3dc # 小于 0 就跳到 default 分支。第一个返回值是 -1 就代表要进入 default 分支
main.go:14 0x10cd3d7 e98c000000 JMP 0x10cd468 # 否则跳到 0x10cd468
main.go:15 0x10cd3dc 0f57c0 XORPS X0, X0
... # 这里省略,是 fmt.Println(3) 的汇编代码
main.go:14 0x10cd456 eb00 JMP 0x10cd458
main.go:14 0x10cd458 488bac2418010000 MOVQ 0x118(SP), BP
main.go:14 0x10cd460 4881c420010000 ADDQ $0x120, SP
main.go:14 0x10cd467 c3 RET
main.go:12 0x10cd468 48837c244800 CMPQ $0x0, 0x48(SP) # 第一个返回值与 0 比较
main.go:12 0x10cd46e 7402 JE 0x10cd472 # 等于 0 就跳转到 testChan1 分支代码
main.go:12 0x10cd470 eb7f JMP 0x10cd4f1 # 否则跳到 0x10cd4f1
main.go:13 0x10cd472 0f57c0 XORPS X0, X0
... # 这里省略,是 fmt.Println(2) 的汇编代码
main.go:12 0x10cd4ec e967ffffff JMP 0x10cd458
main.go:10 0x10cd4f1 48837c244801 CMPQ $0x1, 0x48(SP) # 第一个返回值与 1 比较
main.go:10 0x10cd4f7 7402 JE 0x10cd4fb # 等于 1 就跳转到 testChan 分支代码
main.go:10 0x10cd4f9 eb7f JMP 0x10cd57a # 否则跳到 0x10cd57a
main.go:11 0x10cd4fb 0f57c0 XORPS X0, X0
... # 这里省略,是 fmt.Println(1) 的汇编代码
main.go:10 0x10cd575 e9defeffff JMP 0x10cd458
main.go:10 0x10cd57a 90 NOPL # 无意义填充
main.go:5 0x10cd57b 0f1f440000 NOPL 0(AX)(AX*1) # 无意义填充
main.go:5 0x10cd580 e83b14faff CALL runtime.morestack_noctxt(SB)
main.go:5 0x10cd585 e936fdffff JMP main.main(SB)

可以发现,select 对于 case 分支的选择是使用了 runtime.selectgo(SB) 方法

runtime.selectgo(SB) 方法会执行下面的操作

  1. 依次执行 case 条件,发现可以 send 或者 receive 的 case 的话,就立马执行那个 case
  2. 如果没有一个 case 条件可以 send 或者 receive,则判断有没有 default 分支,有的话直接执行 default 分支,没有的话接着往下
  3. 把当前 g 复制并塞到每个 case 条件中的 channel 的等待队列中,然后休眠当前 g,进入调度
  4. 当某个 case 中的 channel 能够 send 或者 receive 了,这里这个 case 就会被唤醒,然后执行 case 中的语句。这里就存在了一个随机性,如果多个 case 中的 channel 一同解禁(能够 send 或者 receive 了)了,那么谁先执行就取决于哪个 channel 中的 g 被先唤醒了,当然多核下也可能并发

总结

  1. 首先检查所有 case 中的 channel 是否存在解锁的,不存在就立马执行 default 分支,如果不存在 default 分支,则 g 进入休眠等待唤醒
  2. case 中的选择是随机性的,取决于哪个 case 中的 g 被先唤醒,当然多核下也可能同时并发唤醒



微信关注我,及时接收最新技术文章