Golang最大的特色能够说是协程(goroutine)了, 协程让原本很简单的异步编程变得简略, 让程序员不再须要面对回调天堂,
尽管当初引入了协程的语言越来越多, 但go中的协程依然是实现的是最彻底的.
这篇文章将通过剖析golang的源代码来解说协程的实现原理.
这个系列剖析的golang源代码是Google官网的实现的1.9.2版本, 不适用于其余版本和gccgo等其余实现,
运行环境是Ubuntu 16.04 LTS 64bit.
外围概念
要了解协程的实现, 首先须要理解go中的三个十分重要的概念, 它们别离是G, M和P,
没有看过golang源代码的可能会对它们感到生疏, 这三项是协程最次要的组成部分, 它们在golang的源代码中无处不在.
G (goroutine)
G是goroutine的头文字, goroutine能够解释为受治理的轻量线程, goroutine应用go
关键词创立.
举例来说, func main() { go other() }
, 这段代码创立了两个goroutine,
一个是main, 另一个是other, 留神main自身也是一个goroutine.
goroutine的新建, 休眠, 复原, 进行都受到go运行时的治理.
goroutine执行异步操作时会进入休眠状态, 待操作实现后再复原, 无需占用零碎线程,
goroutine新建或复原时会增加到运行队列, 期待M取出并运行.
M (machine)
M是machine的头文字, 在以后版本的golang中等同于零碎线程.
M能够运行两种代码:
- go代码, 即goroutine, M运行go代码须要一个P
- 原生代码, 例如阻塞的syscall, M运行原生代码不须要P
M会从运行队列中取出G, 而后运行G, 如果G运行结束或者进入休眠状态, 则从运行队列中取出下一个G运行, 周而复始.
有时候G须要调用一些无奈防止阻塞的原生代码, 这时M会开释持有的P并进入阻塞状态, 其余M会获得这个P并持续运行队列中的G.
go须要保障有足够的M能够运行G, 不让CPU闲着, 也须要保障M的数量不能过多.
P (process)
P是process的头文字, 代表M运行G所须要的资源.
一些解说协程的文章把P了解为cpu外围, 其实这是谬误的.
尽管P的数量默认等于cpu外围数, 但能够通过环境变量GOMAXPROC
批改, 在理论运行时P跟cpu外围并无任何关联.
P也能够了解为管制go代码的并行度的机制,
如果P的数量等于1, 代表以后最多只能有一个线程(M)执行go代码,
如果P的数量等于2, 代表以后最多只能有两个线程(M)执行go代码.
执行原生代码的线程数量不受P管制.
因为同一时间只有一个线程(M)能够领有P, P中的数据都是锁自在(lock free)的, 读写这些数据的效率会十分的高.
数据结构
在解说协程的工作流程之前, 还须要了解一些外部的数据结构.
G的状态
- 闲暇中(_Gidle): 示意G刚刚新建, 仍未初始化
- 待运行(_Grunnable): 示意G在运行队列中, 期待M取出并运行
- 运行中(_Grunning): 示意M正在运行这个G, 这时候M会领有一个P
- 零碎调用中(_Gsyscall): 示意M正在运行这个G发动的零碎调用, 这时候M并不领有P
- 期待中(_Gwaiting): 示意G在期待某些条件实现, 这时候G不在运行也不在运行队列中(可能在channel的期待队列中)
- 已停止(_Gdead): 示意G未被应用, 可能已执行结束(并在freelist中期待下次复用)
- 栈复制中(_Gcopystack): 示意G正在获取一个新的栈空间并把原来的内容复制过来(用于避免GC扫描)
M的状态
M并没有像G和P一样的状态标记, 但能够认为一个M有以下的状态:
- 自旋中(spinning): M正在从运行队列获取G, 这时候M会领有一个P
- 执行go代码中: M正在执行go代码, 这时候M会领有一个P
- 执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不领有P
- 休眠中: M发现无待运行的G时会进入休眠, 并增加到闲暇M链表中, 这时M并不领有P
自旋中(spinning)这个状态十分重要, 是否须要唤醒或者创立新的M取决于以后自旋中的M的数量.
P的状态
- 闲暇中(_Pidle): 当M发现无待运行的G时会进入休眠, 这时M领有的P会变为闲暇并加到闲暇P链表中
- 运行中(_Prunning): 当M领有了一个P后, 这个P的状态就会变为运行中, M运行G会应用这个P中的资源
- 零碎调用中(_Psyscall): 当go调用原生代码, 原生代码又反过来调用go代码时, 应用的P会变为此状态
- GC进行中(_Pgcstop): 当gc进行了整个世界(STW)时, P会变为此状态
- 已停止(_Pdead): 当P的数量在运行时扭转, 且数量缩小时多余的P会变为此状态
本地运行队列
在go中有多个运行队列能够保留待运行(_Grunnable)的G, 它们别离是各个P中的本地运行队列和全局运行队列.
入队待运行的G时会优先加到以后P的本地运行队列, M获取待运行的G时也会优先从领有的P的本地运行队列获取,
本地运行队列入队和出队不须要应用线程锁.
本地运行队列有数量限度, 当数量达到256个时会入队到全局运行队列.
本地运行队列的数据结构是环形队列, 由一个256长度的数组和两个序号(head, tail)组成.
当M从P的本地运行队列获取G时, 如果发现本地队列为空会尝试从其余P盗取一半的G过去,
这个机制叫做Work Stealing, 详见前面的代码剖析.
全局运行队列
全局运行队列保留在全局变量sched
中, 全局运行队列入队和出队须要应用线程锁.
全局运行队列的数据结构是链表, 由两个指针(head, tail)组成.
闲暇M链表
当M发现无待运行的G时会进入休眠, 并增加到闲暇M链表中, 闲暇M链表保留在全局变量sched
.
进入休眠的M会期待一个信号量(m.park), 唤醒休眠的M会应用这个信号量.
go须要保障有足够的M能够运行G, 是通过这样的机制实现的:
- 入队待运行的G后, 如果以后无自旋的M然而有闲暇的P, 就唤醒或者新建一个M
- 当M来到自旋状态并筹备运行出队的G时, 如果以后无自旋的M然而有闲暇的P, 就唤醒或者新建一个M
- 当M来到自旋状态并筹备休眠时, 会在来到自旋状态后再次查看所有运行队列, 如果有待运行的G则从新进入自旋状态
因为"入队待运行的G"和"M来到自旋状态"会同时进行, go会应用这样的查看程序:
入队待运行的G => 内存屏障 => 查看以后自旋的M数量 => 唤醒或者新建一个M
缩小以后自旋的M数量 => 内存屏障 => 查看所有运行队列是否有待运行的G => 休眠
这样能够保障不会呈现待运行的G入队了, 也有闲暇的资源P, 但无M去执行的状况.
闲暇P链表
当P的本地运行队列中的所有G都运行结束, 又不能从其余中央拿到G时,
领有P的M会开释P并进入休眠状态, 开释的P会变为闲暇状态并加到闲暇P链表中, 闲暇P链表保留在全局变量sched
下次待运行的G入队时如果发现有闲暇的P, 然而又没有自旋中的M时会唤醒或者新建一个M, M会领有这个P, P会从新变为运行中的状态.
工作流程(概览)
下图是协程可能呈现的工作状态, 图中有4个P, 其中M1~M3正在运行G并且运行后会从领有的P的运行队列持续获取G:
只看这张图可能有点难以想象理论的工作流程, 这里我依据理论的代码再解说一遍:
package mainimport ( "fmt" "time")func printNumber(from, to int, c chan int) { for x := from; x <= to; x++ { fmt.Printf("%d\n", x) time.Sleep(1 * time.Millisecond) } c <- 0}func main() { c := make(chan int, 3) go printNumber(1, 3, c) go printNumber(4, 6, c) _ = <- c _ = <- c}
程序启动时会先创立一个G, 指向的是main(理论是runtime.main而不是main.main, 前面解释):
图中的虚线指的是G待运行或者开始运行的地址, 不是以后运行的地址.
M会获得这个G并运行:
这时main会创立一个新的channel, 并启动两个新的G:
接下来G: main
会从channel获取数据, 因为获取不到, G会保留状态并变为期待中(_Gwaiting)并增加到channel的队列:
因为G: main
保留了运行状态, 下次运行时将会从_ = <- c
持续运行.
接下来M会从运行队列获取到G: printNumber
并运行:
printNumber会打印数字, 实现后向channel写数据,
写数据时发现channel中有正在期待的G, 会把数据交给这个G, 把G变为待运行(_Grunnable)并从新放入运行队列:
接下来M会运行下一个G: printNumber
, 因为创立channel时指定了大小为3的缓冲区, 能够间接把数据写入缓冲区而无需期待:
而后printNumber运行结束, 运行队列中就只剩下G: main
了:
最初M把G: main
取出来运行, 会从上次中断的地位_ <- c
持续运行:
第一个_ <- c
的后果曾经在后面设置过了, 这条语句会执行胜利.
第二个_ <- c
在获取时会发现channel中有已缓冲的0, 于是后果就是这个0, 不须要期待.
最初main执行结束, 程序完结.
有人可能会好奇如果最初再加一个_ <- c
会变成什么后果, 这时因为所有G都进入期待状态, go会检测进去并报告死锁:
fatal error: all goroutines are asleep - deadlock!
开始代码剖析
对于概念的解说到此结束, 从这里开始会剖析go中的实现代码, 咱们须要先理解一些根底的内容.
汇编代码
从以下的go代码:
package mainimport ( "fmt" "time")func printNumber(from, to int, c chan int) { for x := from; x <= to; x++ { fmt.Printf("%d\n", x) time.Sleep(1 * time.Millisecond) } c <- 0}func main() { c := make(chan int, 3) go printNumber(1, 3, c) go printNumber(4, 6, c) _, _ = <- c, <- c}
能够生成以下的汇编代码(平台是linux x64, 应用的是默认选项, 即启用优化和内联):
(lldb) di -n main.mainhello`main.main:hello[0x401190] <+0>: movq %fs:-0x8, %rcxhello[0x401199] <+9>: cmpq 0x10(%rcx), %rsphello[0x40119d] <+13>: jbe 0x401291 ; <+257> at hello.go:16hello[0x4011a3] <+19>: subq $0x40, %rsphello[0x4011a7] <+23>: leaq 0xb3632(%rip), %rbx ; runtime.rodata + 38880hello[0x4011ae] <+30>: movq %rbx, (%rsp)hello[0x4011b2] <+34>: movq $0x3, 0x8(%rsp)hello[0x4011bb] <+43>: callq 0x4035a0 ; runtime.makechan at chan.go:49hello[0x4011c0] <+48>: movq 0x10(%rsp), %raxhello[0x4011c5] <+53>: movq $0x1, 0x10(%rsp)hello[0x4011ce] <+62>: movq $0x3, 0x18(%rsp)hello[0x4011d7] <+71>: movq %rax, 0x38(%rsp)hello[0x4011dc] <+76>: movq %rax, 0x20(%rsp)hello[0x4011e1] <+81>: movl $0x18, (%rsp)hello[0x4011e8] <+88>: leaq 0x129c29(%rip), %rax ; main.printNumber.fhello[0x4011ef] <+95>: movq %rax, 0x8(%rsp)hello[0x4011f4] <+100>: callq 0x430cd0 ; runtime.newproc at proc.go:2657hello[0x4011f9] <+105>: movq $0x4, 0x10(%rsp)hello[0x401202] <+114>: movq $0x6, 0x18(%rsp)hello[0x40120b] <+123>: movq 0x38(%rsp), %rbxhello[0x401210] <+128>: movq %rbx, 0x20(%rsp)hello[0x401215] <+133>: movl $0x18, (%rsp)hello[0x40121c] <+140>: leaq 0x129bf5(%rip), %rax ; main.printNumber.fhello[0x401223] <+147>: movq %rax, 0x8(%rsp)hello[0x401228] <+152>: callq 0x430cd0 ; runtime.newproc at proc.go:2657hello[0x40122d] <+157>: movq $0x0, 0x30(%rsp)hello[0x401236] <+166>: leaq 0xb35a3(%rip), %rbx ; runtime.rodata + 38880hello[0x40123d] <+173>: movq %rbx, (%rsp)hello[0x401241] <+177>: movq 0x38(%rsp), %rbxhello[0x401246] <+182>: movq %rbx, 0x8(%rsp)hello[0x40124b] <+187>: leaq 0x30(%rsp), %rbxhello[0x401250] <+192>: movq %rbx, 0x10(%rsp)hello[0x401255] <+197>: callq 0x4043c0 ; runtime.chanrecv1 at chan.go:354hello[0x40125a] <+202>: movq $0x0, 0x28(%rsp)hello[0x401263] <+211>: leaq 0xb3576(%rip), %rbx ; runtime.rodata + 38880hello[0x40126a] <+218>: movq %rbx, (%rsp)hello[0x40126e] <+222>: movq 0x38(%rsp), %rbxhello[0x401273] <+227>: movq %rbx, 0x8(%rsp)hello[0x401278] <+232>: leaq 0x28(%rsp), %rbxhello[0x40127d] <+237>: movq %rbx, 0x10(%rsp)hello[0x401282] <+242>: callq 0x4043c0 ; runtime.chanrecv1 at chan.go:354hello[0x401287] <+247>: movq 0x28(%rsp), %rbxhello[0x40128c] <+252>: addq $0x40, %rsphello[0x401290] <+256>: retq hello[0x401291] <+257>: callq 0x4538d0 ; runtime.morestack_noctxt at asm_amd64.s:365hello[0x401296] <+262>: jmp 0x401190 ; <+0> at hello.go:16hello[0x40129b] <+267>: int3 hello[0x40129c] <+268>: int3 hello[0x40129d] <+269>: int3 hello[0x40129e] <+270>: int3 hello[0x40129f] <+271>: int3 (lldb) di -n main.printNumberhello`main.printNumber:hello[0x401000] <+0>: movq %fs:-0x8, %rcxhello[0x401009] <+9>: leaq -0x8(%rsp), %raxhello[0x40100e] <+14>: cmpq 0x10(%rcx), %raxhello[0x401012] <+18>: jbe 0x401185 ; <+389> at hello.go:8hello[0x401018] <+24>: subq $0x88, %rsphello[0x40101f] <+31>: xorps %xmm0, %xmm0hello[0x401022] <+34>: movups %xmm0, 0x60(%rsp)hello[0x401027] <+39>: movq 0x90(%rsp), %raxhello[0x40102f] <+47>: movq 0x98(%rsp), %rbphello[0x401037] <+55>: cmpq %rbp, %raxhello[0x40103a] <+58>: jg 0x40112f ; <+303> at hello.go:13hello[0x401040] <+64>: movq %rax, 0x40(%rsp)hello[0x401045] <+69>: movq %rax, 0x48(%rsp)hello[0x40104a] <+74>: xorl %ebx, %ebxhello[0x40104c] <+76>: movq %rbx, 0x60(%rsp)hello[0x401051] <+81>: movq %rbx, 0x68(%rsp)hello[0x401056] <+86>: leaq 0x60(%rsp), %rbxhello[0x40105b] <+91>: cmpq $0x0, %rbxhello[0x40105f] <+95>: je 0x40117e ; <+382> at hello.go:10hello[0x401065] <+101>: movq $0x1, 0x78(%rsp)hello[0x40106e] <+110>: movq $0x1, 0x80(%rsp)hello[0x40107a] <+122>: movq %rbx, 0x70(%rsp)hello[0x40107f] <+127>: leaq 0xb73fa(%rip), %rbx ; runtime.rodata + 54400hello[0x401086] <+134>: movq %rbx, (%rsp)hello[0x40108a] <+138>: leaq 0x48(%rsp), %rbxhello[0x40108f] <+143>: movq %rbx, 0x8(%rsp)hello[0x401094] <+148>: movq $0x0, 0x10(%rsp)hello[0x40109d] <+157>: callq 0x40bb90 ; runtime.convT2E at iface.go:128hello[0x4010a2] <+162>: movq 0x18(%rsp), %rcxhello[0x4010a7] <+167>: movq 0x20(%rsp), %raxhello[0x4010ac] <+172>: movq 0x70(%rsp), %rbxhello[0x4010b1] <+177>: movq %rcx, 0x50(%rsp)hello[0x4010b6] <+182>: movq %rcx, (%rbx)hello[0x4010b9] <+185>: movq %rax, 0x58(%rsp)hello[0x4010be] <+190>: cmpb $0x0, 0x19ea1b(%rip) ; time.initdone.hello[0x4010c5] <+197>: jne 0x401167 ; <+359> at hello.go:10hello[0x4010cb] <+203>: movq %rax, 0x8(%rbx)hello[0x4010cf] <+207>: leaq 0xfb152(%rip), %rbx ; go.string.* + 560hello[0x4010d6] <+214>: movq %rbx, (%rsp)hello[0x4010da] <+218>: movq $0x3, 0x8(%rsp)hello[0x4010e3] <+227>: movq 0x70(%rsp), %rbxhello[0x4010e8] <+232>: movq %rbx, 0x10(%rsp)hello[0x4010ed] <+237>: movq 0x78(%rsp), %rbxhello[0x4010f2] <+242>: movq %rbx, 0x18(%rsp)hello[0x4010f7] <+247>: movq 0x80(%rsp), %rbxhello[0x4010ff] <+255>: movq %rbx, 0x20(%rsp)hello[0x401104] <+260>: callq 0x45ad70 ; fmt.Printf at print.go:196hello[0x401109] <+265>: movq $0xf4240, (%rsp) ; imm = 0xF4240 hello[0x401111] <+273>: callq 0x442a50 ; time.Sleep at time.go:48hello[0x401116] <+278>: movq 0x40(%rsp), %raxhello[0x40111b] <+283>: incq %raxhello[0x40111e] <+286>: movq 0x98(%rsp), %rbphello[0x401126] <+294>: cmpq %rbp, %raxhello[0x401129] <+297>: jle 0x401040 ; <+64> at hello.go:10hello[0x40112f] <+303>: movq $0x0, 0x48(%rsp)hello[0x401138] <+312>: leaq 0xb36a1(%rip), %rbx ; runtime.rodata + 38880hello[0x40113f] <+319>: movq %rbx, (%rsp)hello[0x401143] <+323>: movq 0xa0(%rsp), %rbxhello[0x40114b] <+331>: movq %rbx, 0x8(%rsp)hello[0x401150] <+336>: leaq 0x48(%rsp), %rbxhello[0x401155] <+341>: movq %rbx, 0x10(%rsp)hello[0x40115a] <+346>: callq 0x403870 ; runtime.chansend1 at chan.go:99hello[0x40115f] <+351>: addq $0x88, %rsphello[0x401166] <+358>: retq hello[0x401167] <+359>: leaq 0x8(%rbx), %r8hello[0x40116b] <+363>: movq %r8, (%rsp)hello[0x40116f] <+367>: movq %rax, 0x8(%rsp)hello[0x401174] <+372>: callq 0x40f090 ; runtime.writebarrierptr at mbarrier.go:129hello[0x401179] <+377>: jmp 0x4010cf ; <+207> at hello.go:10hello[0x40117e] <+382>: movl %eax, (%rbx)hello[0x401180] <+384>: jmp 0x401065 ; <+101> at hello.go:10hello[0x401185] <+389>: callq 0x4538d0 ; runtime.morestack_noctxt at asm_amd64.s:365hello[0x40118a] <+394>: jmp 0x401000 ; <+0> at hello.go:8hello[0x40118f] <+399>: int3
这些汇编代码当初看不懂也没关系, 上面会从这里取出一部分来解释.
调用标准
不同平台对于函数有不同的调用标准.
例如32位通过栈传递参数, 通过eax寄存器传递返回值.
64位windows通过rcx, rdx, r8, r9传递前4个参数, 通过栈传递第5个开始的参数, 通过eax寄存器传递返回值.
64位linux, unix通过rdi, rsi, rdx, rcx, r8, r9传递前6个参数, 通过栈传递第7个开始的参数, 通过eax寄存器传递返回值.
go并不应用这些调用标准(除非波及到与原生代码交互), go有一套单独的调用标准.
go的调用标准十分的简略, 所有参数都通过栈传递, 返回值也通过栈传递,
例如这样的函数:
type MyStruct struct { X int; P *int }func someFunc(x int, s MyStruct) (int, MyStruct) { ... }
调用函数时的栈的内容如下:
能够看得出参数和返回值都从低位到高位排列, go函数能够有多个返回值的起因也在于此. 因为返回值都通过栈传递了.
须要留神的这里的"返回地址"是x86和x64上的, arm的返回地址会通过LR寄存器保留, 内容会和这里的略微不一样.
另外留神的是和c不一样, 传递结构体时整个结构体的内容都会复制到栈上, 如果结构体很大将会影响性能.
TLS
TLS的全称是Thread-local storage, 代表每个线程的中的本地数据.
例如规范c中的errno就是一个典型的TLS变量, 每个线程都有一个单独的errno, 写入它不会烦扰到其余线程中的值.
go在实现协程时十分依赖TLS机制, 会用于获取零碎线程中以后的G和G所属的M的实例.
因为go并不应用glibc, 操作TLS会应用零碎原生的接口, 以linux x64为例,
go在新建M时会调用arch_prctl这个syscall设置FS寄存器的值为M.tls的地址,
运行中每个M的FS寄存器都会指向它们对应的M实例的tls, linux内核调度线程时FS寄存器会跟着线程一起切换,
这样go代码只须要拜访FS寄存器就能够存取线程本地的数据.
下面的汇编代码中的
hello[0x401000] <+0>: movq %fs:-0x8, %rcx
会把指向以后的G的指针从TLS挪动到rcx寄存器中.
栈扩张
因为go中的协程是stackful coroutine, 每一个goroutine都须要有本人的栈空间,
栈空间的内容在goroutine休眠时须要保留, 待休眠实现后复原(这时整个调用树都是残缺的).
这样就引出了一个问题, goroutine可能会同时存在很多个, 如果每一个goroutine都事后调配一个足够的栈空间那么go就会应用过多的内存.
为了防止这个问题, go在一开始只为goroutine调配一个很小的栈空间, 它的大小在以后版本是2K.
当函数发现栈空间有余时, 会申请一块新的栈空间并把原来的栈内容复制过来.
下面的汇编代码中的
hello[0x401000] <+0>: movq %fs:-0x8, %rcxhello[0x401009] <+9>: leaq -0x8(%rsp), %raxhello[0x40100e] <+14>: cmpq 0x10(%rcx), %raxhello[0x401012] <+18>: jbe 0x401185 ; <+389> at hello.go:8
会查看比拟rsp减去肯定值当前是否比g.stackguard0小, 如果小于等于则须要调到上面调用morestack_noctxt函数.
仔细的可能会发现比拟的值跟理论减去的值不统一, 这是因为stackguard0上面会预留一小部分空间, 编译时确定不超过预留的空间能够省略比对.
写屏障(Write Barrier)
因为go反对并行GC, GC的扫描和go代码能够同时运行, 这样带来的问题是GC扫描的过程中go代码有可能扭转了对象的依赖树,
例如开始扫描时发现根对象A和B, B领有C的指针, GC先扫描A, 而后B把C的指针交给A, GC再扫描B, 这时C就不会被扫描到.
为了防止这个问题, go在GC的标记阶段会启用写屏障(Write Barrier).
启用了写屏障(Write Barrier)后, 当B把C的指针交给A时, GC会认为在这一轮的扫描中C的指针是存活的,
即便A可能会在稍后丢掉C, 那么C就在下一轮回收.
写屏障只针对指针启用, 而且只在GC的标记阶段启用, 平时会间接把值写入到指标地址:
对于写屏障的具体将在下一篇(GC篇)剖析.
值得一提的是CoreCLR的GC也有写屏障的机制, 但作用跟这里的不一样(用于标记跨代援用).
闭包(Closure)
闭包这个概念自身应该不须要解释, 咱们理论看一看go是如何实现闭包的:
package mainimport ( "fmt")func executeFn(fn func() int) int { return fn();}func main() { a := 1 b := 2 c := executeFn(func() int { a += b return a }) fmt.Printf("%d %d %d\n", a, b, c)}
这段代码的输入后果是3 2 3
, 相熟go的应该不会感到意外.
main函数执行executeFn函数的汇编代码如下:
hello[0x4a096f] <+47>: movq $0x1, 0x40(%rsp) ; 变量a等于1hello[0x4a0978] <+56>: leaq 0x151(%rip), %rax ; 寄存器rax等于匿名函数main.main.func1的地址hello[0x4a097f] <+63>: movq %rax, 0x60(%rsp) ; 变量rsp+0x60等于匿名函数的地址hello[0x4a0984] <+68>: leaq 0x40(%rsp), %rax ; 寄存器rax等于变量a的地址hello[0x4a0989] <+73>: movq %rax, 0x68(%rsp) ; 变量rsp+0x68等于变量a的地址hello[0x4a098e] <+78>: movq $0x2, 0x70(%rsp) ; 变量rsp+0x70等于2(变量b的值)hello[0x4a0997] <+87>: leaq 0x60(%rsp), %rax ; 寄存器rax等于地址rsp+0x60hello[0x4a099c] <+92>: movq %rax, (%rsp) ; 第一个参数等于地址rsp+0x60hello[0x4a09a0] <+96>: callq 0x4a08f0 ; 执行main.executeFnhello[0x4a09a5] <+101>: movq 0x8(%rsp), %rax ; 寄存器rax等于返回值
咱们能够看到传给executeFn的是一个指针, 指针指向的内容是[匿名函数的地址, 变量a的地址, 变量b的值]
.
变量a传地址的起因是匿名函数中对a进行了批改, 须要反映到原来的a上.
executeFn函数执行闭包的汇编代码如下:
hello[0x4a08ff] <+15>: subq $0x10, %rsp ; 在栈上调配0x10的空间hello[0x4a0903] <+19>: movq %rbp, 0x8(%rsp) ; 把原来的寄存器rbp移到变量rsp+0x8hello[0x4a0908] <+24>: leaq 0x8(%rsp), %rbp ; 把变量rsp+0x8的地址移到寄存器rbphello[0x4a090d] <+29>: movq 0x18(%rsp), %rdx ; 把第一个参数(闭包)的指针移到寄存器rdxhello[0x4a0912] <+34>: movq (%rdx), %rax ; 把闭包中函数的指针移到寄存器raxhello[0x4a0915] <+37>: callq *%rax ; 调用闭包中的函数hello[0x4a0917] <+39>: movq (%rsp), %rax ; 把返回值移到寄存器raxhello[0x4a091b] <+43>: movq %rax, 0x20(%rsp) ; 把寄存器rax移到返回值中(参数前面)hello[0x4a0920] <+48>: movq 0x8(%rsp), %rbp ; 把变量rsp+0x8的值复原寄存器rbp(恢复原rbp)hello[0x4a0925] <+53>: addq $0x10, %rsp ; 开释栈空间hello[0x4a0929] <+57>: retq ; 从函数返回
能够看到调用闭包时参数并不通过栈传递, 而是通过寄存器rdx传递, 闭包的汇编代码如下:
hello[0x455660] <+0>: movq 0x8(%rdx), %rax ; 第一个参数移到寄存器rax(变量a的指针)hello[0x455664] <+4>: movq (%rax), %rcx ; 把寄存器rax指向的值移到寄存器rcx(变量a的值)hello[0x455667] <+7>: addq 0x10(%rdx), %rcx ; 增加第二个参数到寄存器rcx(变量a的值+变量b的值)hello[0x45566b] <+11>: movq %rcx, (%rax) ; 把寄存器rcx移到寄存器rax指向的值(相加的后果保留回变量a)hello[0x45566e] <+14>: movq %rcx, 0x8(%rsp) ; 把寄存器rcx移到返回后果hello[0x455673] <+19>: retq ; 从函数返回
闭包的传递能够总结如下:
- 闭包的内容是[匿名函数的地址, 传给匿名函数的参数(不定长)...]
- 传递闭包给其余函数时会传递指向"闭包的内容"的指针
- 调用闭包时会把指向"闭包的内容"的指针放到寄存器rdx(在go外部这个指针称为"上下文")
- 闭包会从寄存器rdx取出参数
- 如果闭包批改了变量, 闭包中的参数会是指针而不是值, 批改时会批改到原来的地位上
闭包+goroutine
仔细的可能会发现在下面的例子中, 闭包的内容在栈上, 如果不是间接调用executeFn而是go executeFn呢?
把下面的代码改为go executeFn(func() ...)
能够生成以下的汇编代码:
hello[0x455611] <+33>: leaq 0xb4a8(%rip), %rax ; 寄存器rax等于类型信息hello[0x455618] <+40>: movq %rax, (%rsp) ; 第一个参数等于类型信息hello[0x45561c] <+44>: callq 0x40d910 ; 调用runtime.newobjecthello[0x455621] <+49>: movq 0x8(%rsp), %rax ; 寄存器rax等于返回值(这里称为新对象a)hello[0x455626] <+54>: movq %rax, 0x28(%rsp) ; 变量rsp+0x28等于新对象ahello[0x45562b] <+59>: movq $0x1, (%rax) ; 新对象a的值等于1hello[0x455632] <+66>: leaq 0x136e7(%rip), %rcx ; 寄存器rcx等于类型信息hello[0x455639] <+73>: movq %rcx, (%rsp) ; 第一个参数等于类型信息hello[0x45563d] <+77>: callq 0x40d910 ; 调用runtime.newobjecthello[0x455642] <+82>: movq 0x8(%rsp), %rax ; 寄存器rax等于返回值(这里称为新对象fn)hello[0x455647] <+87>: leaq 0x82(%rip), %rcx ; 寄存器rcx等于匿名函数main.main.func1的地址hello[0x45564e] <+94>: movq %rcx, (%rax) ; 新对象fn+0的值等于main.main.func1的地址hello[0x455651] <+97>: testb (%rax), %al ; 确保新对象fn不等于nilhello[0x455653] <+99>: movl 0x78397(%rip), %ecx ; 寄存器ecx等于以后是否启用写屏障hello[0x455659] <+105>: leaq 0x8(%rax), %rdx ; 寄存器rdx等于新对象fn+0x8的地址hello[0x45565d] <+109>: testl %ecx, %ecx ; 判断以后是否启用写屏障hello[0x45565f] <+111>: jne 0x455699 ; 启用写屏障时调用前面的逻辑hello[0x455661] <+113>: movq 0x28(%rsp), %rcx ; 寄存器rcx等于新对象ahello[0x455666] <+118>: movq %rcx, 0x8(%rax) ; 设置新对象fn+0x8的值等于新对象ahello[0x45566a] <+122>: movq $0x2, 0x10(%rax) ; 设置新对象fn+0x10的值等于2(变量b的值)hello[0x455672] <+130>: movq %rax, 0x10(%rsp) ; 第三个参数等于新对象fn(额定参数)hello[0x455677] <+135>: movl $0x10, (%rsp) ; 第一个参数等于0x10(函数+参数的大小)hello[0x45567e] <+142>: leaq 0x22fb3(%rip), %rax ; 第二个参数等于一个常量结构体的地址hello[0x455685] <+149>: movq %rax, 0x8(%rsp) ; 这个结构体的类型是funcval, 值是executeFn的地址hello[0x45568a] <+154>: callq 0x42e690 ; 调用runtime.newproc创立新的goroutine
咱们能够看到goroutine+闭包的状况更简单, 首先go会通过逃逸剖析算出变量a和闭包会逃逸到里面,
这时go会在heap上调配变量a和闭包, 下面调用的两次newobject就是别离对变量a和闭包的调配.
在创立goroutine时, 首先会传入函数+参数的大小(下面是8+8=16), 而后传入函数+参数, 下面的参数即闭包的地址.
m0和g0
go中还有非凡的M和G, 它们是m0和g0.
m0是启动程序后的主线程, 这个m对应的实例会在全局变量m0中, 不须要在heap上调配,
m0负责执行初始化操作和启动第一个g, 在之后m0就和其余的m一样了.
g0是仅用于负责调度的G, g0不指向任何可执行的函数, 每个m都会有一个本人的g0,
在调度或零碎调用时会应用g0的栈空间, 全局变量的g0是m0的g0.
如果下面的内容都理解, 就能够开始看golang的源代码了.
程序初始化
go程序的入口点是runtime.rt0_go, 流程是:
- 调配栈空间, 须要2个本地变量+2个函数参数, 而后向8对齐
- 把传入的argc和argv保留到栈上
- 更新g0中的stackguard的值, stackguard用于检测栈空间是否有余, 须要调配新的栈空间
- 获取以后cpu的信息并保留到各个全局变量
- 调用_cgo_init如果函数存在
- 初始化以后线程的TLS, 设置FS寄存器为m0.tls+8(获取时会-8)
- 测试TLS是否工作
- 设置g0到TLS中, 示意以后的g是g0
- 设置m0.g0 = g0
- 设置g0.m = m0
- 调用runtime.check做一些查看
- 调用runtime.args保留传入的argc和argv到全局变量
调用runtime.osinit依据零碎执行不同的初始化
- 这里(linux x64)设置了全局变量ncpu等于cpu外围数量
调用runtime.schedinit执行独特的初始化
- 这里的解决比拟多, 会初始化栈空间分配器, GC, 按cpu外围数量或GOMAXPROCS的值生成P等
- 生成P的解决在procresize中
调用runtime.newproc创立一个新的goroutine, 指向的是
runtime.main
- runtime.newproc这个函数在创立一般的goroutine时也会应用, 在上面的"go的实现"中会具体解说
调用runtime·mstart启动m0
- 启动后m0会一直从运行队列获取G并运行, runtime.mstart调用后不会返回
- runtime.mstart这个函数是m的入口点(不仅仅是m0), 在上面的"调度器的实现"中会具体解说
第一个被调度的G会运行runtime.main, 流程是:
- 标记主函数已调用, 设置mainStarted = true
- 启动一个新的M执行sysmon函数, 这个函数会监控全局的状态并对运行工夫过长的G进行抢占
- 要求G必须在以后M(零碎主线程)上执行
- 调用runtime_init函数
- 调用gcenable函数
- 调用main.init函数, 如果函数存在
- 不再要求G必须在以后M上运行
- 如果程序是作为c的类库编译的, 在这里返回
- 调用main.main函数
- 如果以后产生了panic, 则期待panic解决
- 调用exit(0)退出程序
G M P的定义
G的定义在这里.
M的定义在这里.
P的定义在这里.
G外面比拟重要的成员如下
- stack: 以后g应用的栈空间, 有lo和hi两个成员
- stackguard0: 查看栈空间是否足够的值, 低于这个值会扩张栈, 0是go代码应用的
- stackguard1: 查看栈空间是否足够的值, 低于这个值会扩张栈, 1是原生代码应用的
- m: 以后g对应的m
- sched: g的调度数据, 当g中断时会保留以后的pc和rsp等值到这里, 复原运行时会应用这里的值
- atomicstatus: g的以后状态
- schedlink: 下一个g, 当g在链表构造中会应用
- preempt: g是否被抢占中
- lockedm: g是否要求要回到这个M执行, 有的时候g中断了复原会要求应用原来的M执行
M外面比拟重要的成员如下
- g0: 用于调度的非凡g, 调度和执行零碎调用时会切换到这个g
- curg: 以后运行的g
- p: 以后领有的P
- nextp: 唤醒M时, M会领有这个P
- park: M休眠时应用的信号量, 唤醒M时会通过它唤醒
- schedlink: 下一个m, 当m在链表构造中会应用
- mcache: 分配内存时应用的本地分配器, 和p.mcache一样(领有P时会复制过去)
- lockedg: lockedm的对应值
P外面比拟重要的成员如下
- status: p的以后状态
- link: 下一个p, 当p在链表构造中会应用
- m: 领有这个P的M
- mcache: 分配内存时应用的本地分配器
- runqhead: 本地运行队列的出队序号
- runqtail: 本地运行队列的入队序号
- runq: 本地运行队列的数组, 能够保留256个G
- gfree: G的自在列表, 保留变为_Gdead后能够复用的G实例
- gcBgMarkWorker: 后盾GC的worker函数, 如果它存在M会优先执行它
- gcw: GC的本地工作队列, 具体将在下一篇(GC篇)剖析
go的实现
应用go命令创立goroutine时, go会把go命令编译为对runtime.newproc的调用, 堆栈的构造如下:
第一个参数是funcval + 额定参数的长度, 第二个参数是funcval, 前面的都是传递给goroutine中执行的函数的额定参数.
funcval的定义在这里, fn是指向函数机器代码的指针.
runtime.newproc的解决如下:
- 计算额定参数的地址argp
- 获取调用端的地址(返回地址)pc
- 应用systemstack调用newproc1
systemstack会切换以后的g到g0, 并且应用g0的栈空间, 而后调用传入的函数, 再切换回原来的g和原来的栈空间.
切换到g0后会伪装返回地址是mstart, 这样traceback的时候能够在mstart进行.
这里传给systemstack的是一个闭包, 调用时会把闭包的地址放到寄存器rdx, 具体能够参考上面对闭包的剖析.
runtime.newproc1的解决如下:
- 调用getg获取以后的g, 会编译为读取FS寄存器(TLS), 这里会获取到g0
- 设置g对应的m的locks++, 禁止抢占
- 获取m领有的p
新建一个g
- 首先调用gfget从p.gfree获取g, 如果之前有g被回收在这里就能够复用
- 获取不到时调用malg调配一个g, 初始的栈空间大小是2K
- 须要先设置g的状态为已停止(_Gdead), 这样gc不会去扫描这个g的未初始化的栈
- 把参数复制到g的栈上
- 把返回地址复制到g的栈上, 这里的返回地址是goexit, 示意调用完指标函数后会调用goexit
设置g的调度数据(sched)
- 设置sched.sp等于参数+返回地址后的rsp地址
- 设置sched.pc等于指标函数的地址, 查看gostartcallfn和gostartcall
- 设置sched.g等于g
- 设置g的状态为待运行(_Grunnable)
调用runqput把g放到运行队列
- 首先随机把g放到p.runnext, 如果放到runnext则入队原来在runnext的g
- 而后尝试把g放到P的"本地运行队列"
如果本地运行队列满了则调用runqputslow把g放到"全局运行队列"
- runqputslow会把本地运行队列中一半的g放到全局运行队列, 这样下次就能够持续用疾速的本地运行队列了
如果以后有闲暇的P, 然而无自旋的M(nmspinning等于0), 并且主函数已执行则唤醒或新建一个M
- 这一步十分重要, 用于保障以后有足够的M运行G, 具体请查看下面的"闲暇M链表"
唤醒或新建一个M会通过wakep函数
- 首先替换nmspinning到1, 胜利再持续, 多个线程同时执行wakep只有一个会持续
调用startm函数
- 调用pidleget从"闲暇P链表"获取一个闲暇的P
- 调用mget从"闲暇M链表"获取一个闲暇的M
如果没有闲暇的M, 则调用newm新建一个M
- newm会新建一个m的实例, m的实例蕴含一个g0, 而后调用newosproc动一个零碎线程
- newosproc会调用syscall clone创立一个新的线程
- 线程创立后会设置TLS, 设置TLS中以后的g为g0, 而后执行mstart
- 调用notewakeup(&mp.park)唤醒线程
创立goroutine的流程就这么多了, 接下来看看M是如何调度的.
调度器的实现
M启动时会调用mstart函数, m0在初始化后调用, 其余的的m在线程启动后调用.
mstart函数的解决如下:
- 调用getg获取以后的g, 这里会获取到g0
- 如果g未调配栈则从以后的栈空间(零碎栈空间)上调配, 也就是说g0会应用零碎栈空间
调用mstart1函数
- 调用gosave函数保留以后的状态到g0的调度数据中, 当前每次调度都会从这个栈地址开始
- 调用asminit函数, 不做任何事件
- 调用minit函数, 设置以后线程能够接管的信号(signal)
- 调用schedule函数
调用schedule函数后就进入了调度循环, 整个流程能够简略总结为:
schedule函数获取g => [必要时休眠] => [唤醒后持续获取] => execute函数执行g => 执行后返回到goexit => 从新执行schedule函数
schedule函数的解决如下:
- 如果以后GC须要进行整个世界(STW), 则调用stopm休眠以后的M
- 如果M领有的P中指定了须要在平安点运行的函数(P.runSafePointFn), 则运行它
疾速获取待运行的G, 以下解决如果有一个获取胜利前面就不会持续获取
- 如果以后GC正在标记阶段, 则查找有没有待运行的GC Worker, GC Worker也是一个G
- 为了偏心起见, 每61次调度从全局运行队列获取一次G, (始终从本地获取可能导致全局运行队列中的G不被运行)
- 从P的本地运行队列中获取G, 调用runqget函数
疾速获取失败时, 调用findrunnable函数获取待运行的G, 会阻塞到获取胜利为止
- 如果以后GC须要进行整个世界(STW), 则调用stopm休眠以后的M
- 如果M领有的P中指定了须要在平安点运行的函数(P.runSafePointFn), 则运行它
- 如果有析构器待运行则应用"运行析构器的G"
- 从P的本地运行队列中获取G, 调用runqget函数
- 从全局运行队列获取G, 调用globrunqget函数, 须要上锁
- 从网络事件反应器获取G, 函数netpoll会获取哪些fd可读可写或已敞开, 而后返回期待fd相干事件的G
如果获取不到G, 则执行Work Stealing
- 调用runqsteal尝试从其余P的本地运行队列盗取一半的G
如果还是获取不到G, 就须要休眠M了, 接下来是休眠的步骤
- 再次查看以后GC是否在标记阶段, 在则查找有没有待运行的GC Worker, GC Worker也是一个G
- 再次查看如果以后GC须要进行整个世界, 或者P指定了须要再平安点运行的函数, 则跳到findrunnable的顶部重试
- 再次查看全局运行队列中是否有G, 有则获取并返回
- 开释M领有的P, P会变为闲暇(_Pidle)状态
- 把P增加到"闲暇P链表"中
- 让M来到自旋状态, 这里的解决十分重要, 参考下面的"闲暇M链表"
- 首先缩小示意以后自旋中的M的数量的全局变量nmspinning
- 再次查看所有P的本地运行队列, 如果不为空则让M从新进入自旋状态, 并跳到findrunnable的顶部重试
- 再次查看有没有待运行的GC Worker, 有则让M从新进入自旋状态, 并跳到findrunnable的顶部重试
- 再次查看网络事件反应器是否有待运行的G, 这里对netpoll的调用会阻塞, 直到某个fd收到了事件
- 如果最终还是获取不到G, 调用stopm休眠以后的M
- 唤醒后跳到findrunnable的顶部重试
- 胜利获取到一个待运行的G
让M来到自旋状态, 调用resetspinning, 这里的解决和下面的不一样
- 如果以后有闲暇的P, 然而无自旋的M(nmspinning等于0), 则唤醒或新建一个M
- 下面来到自旋状态是为了休眠M, 所以会再次查看所有队列而后休眠
- 这里来到自选状态是为了执行G, 所以会查看是否有闲暇的P, 有则示意能够再开新的M执行G
如果G要求回到指定的M(例如下面的runtime.main)
- 调用startlockedm函数把G和P交给该M, 本人进入休眠
- 从休眠唤醒后跳到schedule的顶部重试
- 调用execute函数执行G
execute函数的解决如下:
- 调用getg获取以后的g
- 把G的状态由待运行(_Grunnable)改为运行中(_Grunning)
- 设置G的stackguard, 栈空间有余时能够扩张
- 减少P中记录的调度次数(对应下面的每61次优先获取一次全局运行队列)
- 设置g.m.curg = g
- 设置g.m = m
调用gogo函数
- 这个函数会依据g.sched中保留的状态复原各个寄存器的值并持续运行g
- 首先针对g.sched.ctxt调用写屏障(GC标记指针存活), ctxt中个别会保留指向[函数+参数]的指针
- 设置TLS中的g为g.sched.g, 也就是g本身
- 设置rsp寄存器为g.sched.rsp
- 设置rax寄存器为g.sched.ret
- 设置rdx寄存器为g.sched.ctxt (上下文)
- 设置rbp寄存器为g.sched.rbp
- 清空sched中保留的信息
- 跳转到g.sched.pc
- 因为后面创立goroutine的newproc1函数把返回地址设为了goexit, 函数运行结束返回时将会调用goexit函数
g.sched.pc在G首次运行时会指向指标函数的第一条机器指令,
如果G被抢占或者期待资源而进入休眠, 在休眠前会保留状态到g.sched,
g.sched.pc会变为唤醒后须要继续执行的地址, "保留状态"的实现将在上面解说.
指标函数执行结束后会调用goexit函数, goexit函数会调用goexit1函数, goexit1函数会通过mcall调用goexit0函数.
mcall这个函数就是用于实现"保留状态"的, 解决如下:
- 设置g.sched.pc等于以后的返回地址
- 设置g.sched.sp等于寄存器rsp的值
- 设置g.sched.g等于以后的g
- 设置g.sched.bp等于寄存器rbp的值
- 切换TLS中以后的g等于m.g0
- 设置寄存器rsp等于g0.sched.sp, 应用g0的栈空间
- 设置第一个参数为原来的g
- 设置rdx寄存器为指向函数地址的指针(上下文)
- 调用指定的函数, 不会返回
mcall这个函数保留以后的运行状态到g.sched, 而后切换到g0和g0的栈空间, 再调用指定的函数.
回到g0的栈空间这个步骤十分重要, 因为这个时候g曾经中断, 持续应用g的栈空间且其余M唤醒了这个g将会产生灾难性的结果.
G在中断或者完结后都会通过mcall回到g0的栈空间持续调度, 从goexit调用的mcall的保留状态其实是多余的, 因为G曾经完结了.
goexit1函数会通过mcall调用goexit0函数, goexit0函数调用时曾经回到了g0的栈空间, 解决如下:
- 把G的状态由运行中(_Grunning)改为已停止(_Gdead)
- 清空G的成员
- 调用dropg函数解除M和G之间的关联
- 调用gfput函数把G放到P的自在列表中, 下次创立G时能够复用
- 调用schedule函数持续调度
G完结后回到schedule函数, 这样就完结了一个调度循环.
不仅只有G完结会从新开始调度, G被抢占或者期待资源也会从新进行调度, 上面持续来看这两种状况.
抢占的实现
下面我提到了runtime.main会创立一个额定的M运行sysmon函数, 抢占就是在sysmon中实现的.
sysmon会进入一个有限循环, 第一轮回休眠20us, 之后每次休眠工夫倍增, 最终每一轮都会休眠10ms.
sysmon中有netpool(获取fd事件), retake(抢占), forcegc(按工夫强制执行gc), scavenge heap(开释自在列表中多余的项缩小内存占用)等解决.
retake函数负责解决抢占, 流程是:
枚举所有的P
如果P在零碎调用中(_Psyscall), 且通过了一次sysmon循环(20us~10ms), 则抢占这个P
- 调用handoffp解除M和P之间的关联
如果P在运行中(_Prunning), 且通过了一次sysmon循环并且G运行工夫超过forcePreemptNS(10ms), 则抢占这个P
调用preemptone函数
- 设置g.preempt = true
- 设置g.stackguard0 = stackPreempt
为什么设置了stackguard就能够实现抢占?
因为这个值用于查看以后栈空间是否足够, go函数的结尾会比对这个值判断是否须要扩张栈.
stackPreempt是一个非凡的常量, 它的值会比任何的栈地址都要大, 查看时肯定会触发栈扩张.
栈扩张调用的是morestack_noctxt函数, morestack_noctxt函数清空rdx寄存器并调用morestack函数.
morestack函数会保留G的状态到g.sched, 切换到g0和g0的栈空间, 而后调用newstack函数.
newstack函数判断g.stackguard0等于stackPreempt, 就晓得这是抢占触发的, 这时会再查看一遍是否要抢占:
- 如果M被锁定(函数的本地变量中有P), 则跳过这一次的抢占并调用gogo函数持续运行G
- 如果M正在分配内存, 则跳过这一次的抢占并调用gogo函数持续运行G
- 如果M设置了以后不能抢占, 则跳过这一次的抢占并调用gogo函数持续运行G
- 如果M的状态不是运行中, 则跳过这一次的抢占并调用gogo函数持续运行G
即便这一次抢占失败, 因为g.preempt等于true, runtime中的一些代码会从新设置stackPreempt以重试下一次的抢占.
如果判断能够抢占, 则持续判断是否GC引起的, 如果是则对G的栈空间执行标记解决(扫描根对象)而后持续运行,
如果不是GC引起的则调用gopreempt_m函数实现抢占.
gopreempt_m函数会调用goschedImpl函数, goschedImpl函数的流程是:
- 把G的状态由运行中(_Grunnable)改为待运行(_Grunnable)
- 调用dropg函数解除M和G之间的关联
- 调用globrunqput把G放到全局运行队列
- 调用schedule函数持续调度
因为全局运行队列的优先度比拟低, 各个M会通过一段时间再去从新获取这个G执行,
抢占机制保障了不会有一个G长时间的运行导致其余G无奈运行的状况产生.
channel的实现
在goroutine运行的过程中, 有时候须要对资源进行期待, channel就是最典型的资源.
channel的数据定义在这里, 其中要害的成员如下:
- qcount: 以后队列中的元素数量
- dataqsiz: 队列能够包容的元素数量, 如果为0示意这个channel无缓冲区
- buf: 队列的缓冲区, 构造是环形队列
- elemsize: 元素的大小
- closed: 是否已敞开
- elemtype: 元素的类型, 判断是否调用写屏障时应用
- sendx: 发送元素的序号
- recvx: 接管元素的序号
- recvq: 以后期待从channel接收数据的G的链表(理论类型是sudog的链表)
- sendq: 以后期待发送数据到channel的G的链表(理论类型是sudog的链表)
- lock: 操作channel时应用的线程锁
发送数据到channel理论调用的是runtime.chansend1函数, chansend1函数调用了chansend函数, 流程是:
查看channel.recvq是否有期待中的接收者的G
- 如果有, 示意channel无缓冲区或者缓冲区为空
调用send函数
- 如果sudog.elem不等于nil, 调用sendDirect函数从发送者间接复制元素
- 期待接管的sudog.elem是指向接管指标的内存的指针, 如果是接管指标是
_
则elem是nil, 能够省略复制 - 期待发送的sudog.elem是指向起源指标的内存的指针
复制后调用goready复原发送者的G
切换到g0调用ready函数, 调用完切换回来
- 把G的状态由期待中(_Gwaiting)改为待运行(_Grunnable)
- 把G放到P的本地运行队列
- 如果以后有闲暇的P, 然而无自旋的M(nmspinning等于0), 则唤醒或新建一个M
- 从发送者拿到数据并唤醒了G后, 就能够从chansend返回了
判断是否能够把元素放到缓冲区中
- 如果缓冲区有空余的空间, 则把元素放到缓冲区并从chansend返回
无缓冲区或缓冲区曾经写满, 发送者的G须要期待
- 获取以后的g
- 新建一个sudog
- 设置sudog.elem = 指向发送内存的指针
- 设置sudog.g = g
- 设置sudog.c = channel
- 设置g.waiting = sudog
- 把sudog放入channel.sendq
调用goparkunlock函数
调用gopark函数
通过mcall函数调用park_m函数
- mcall函数和下面阐明的一样, 会把以后的状态保留到g.sched, 而后切换到g0和g0的栈空间并执行指定的函数
- park_m函数首先把G的状态从运行中(_Grunning)改为期待中(_Gwaiting)
- 而后调用dropg函数解除M和G之间的关联
- 再调用传入的解锁函数, 这里的解锁函数会对解除channel.lock的锁定
- 最初调用schedule函数持续调度
从这里复原示意曾经胜利发送或者channel已敞开
- 查看sudog.param是否为nil, 如果为nil示意channel已敞开, 抛出panic
- 否则开释sudog而后返回
从channel接收数据理论调用的是runtime.chanrecv1函数, chanrecv1函数调用了chanrecv函数, 流程是:
查看channel.sendq中是否有期待中的发送者的G
- 如果有, 示意channel无缓冲区或者缓冲区已满, 这两种状况须要别离解决(为了保障入出队程序统一)
调用recv函数
- 如果无缓冲区, 调用recvDirect函数把元素间接复制给接收者
如果有缓冲区代表缓冲区已满
- 把队列中下一个要出队的元素间接复制给接收者
- 把发送的元素复制到队列中方才出队的地位
- 这时候缓冲区依然是满的, 然而发送序号和接管序号都会减少1
- 复制后调用goready复原接收者的G, 解决同上
- 把数据交给接收者并唤醒了G后, 就能够从chanrecv返回了
判断是否能够从缓冲区获取元素
- 如果缓冲区有元素, 则间接取出该元素并从chanrecv返回
无缓冲区或缓冲区无元素, 接收者的G须要期待
- 获取以后的g
- 新建一个sudog
- 设置sudog.elem = 指向接管内存的指针
- 设置sudog.g = g
- 设置sudog.c = channel
- 设置g.waiting = sudog
- 把sudog放入channel.recvq
- 调用goparkunlock函数, 解决同上
从这里复原示意曾经胜利接管或者channel已敞开
- 查看sudog.param是否为nil, 如果为nil示意channel已敞开
- 和发送不一样的是接管不会抛panic, 会通过返回值告诉channel已敞开
- 开释sudog而后返回
敞开channel理论调用的是closechan函数, 流程是:
- 设置channel.closed = 1
- 枚举channel.recvq, 清零它们sudog.elem, 设置sudog.param = nil
- 枚举channel.sendq, 设置sudog.elem = nil, 设置sudog.param = nil
- 调用goready函数复原所有接收者和发送者的G
能够看到如果G须要期待资源时,
会记录G的运行状态到g.sched, 而后把状态改为期待中(_Gwaiting), 再让以后的M持续运行其余G.
期待中的G保留在哪里, 什么时候复原是期待的资源决定的, 上面对channel的期待会让G放到channel中的链表.
对网络资源的期待能够看netpoll相干的解决, netpoll在不同零碎中的解决都不一样, 有趣味的能够本人看看.
参考链接
https://github.com/golang/go
https://golang.org/s/go11sched
http://supertech.csail.mit.edu/papers/steal.pdf
https://docs.google.com/document/d/1ETuA2IOmnaQ4j81AtTGT40Y4_Jr6_IDASEKg0t0dBR8/edit#heading=h.x4kziklnb8fr
https://blog.altoros.com/golang-part-1-main-concepts-and-project-structure.html
https://blog.altoros.com/golang-internals-part-2-diving-into-the-go-compiler.html
https://blog.altoros.com/golang-internals-part-3-the-linker-and-object-files.html
https://blog.altoros.com/golang-part-4-object-files-and-function-metadata.html
https://blog.altoros.com/golang-internals-part-5-runtime-bootstrap-process.html
https://blog.altoros.com/golang-internals-part-6-bootstrapping-and-memory-allocator-initialization.html
http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64
http://legendtkl.com/categories/golang
http://www.cnblogs.com/diegodu/p/5803202.html
https://www.douban.com/note/300631999/
http://morsmachine.dk/go-scheduler
legendtkl很早就曾经开始写golang外部实现相干的文章了, 他的文章很有参考价值, 倡议同时浏览他写的内容.
morsmachine写的针对协程的剖析也倡议参考.
golang中的协程实现十分的清晰, 在这里要再次拜服google工程师的功力, 能够写出这样简略易懂的代码不容易.