Golang最大的特点能够说是协程(goroutine)了, 协程让原本很复杂的异步编程变得简单, 让程序员再也不须要面对回调地狱,
虽然如今引入了协程的语言愈来愈多, 但go中的协程仍然是实现的是最完全的.
这篇文章将经过分析golang的源代码来说解协程的实现原理.html
这个系列分析的golang源代码是Google官方的实现的1.9.2版本, 不适用于其余版本和gccgo等其余实现,
运行环境是Ubuntu 16.04 LTS 64bit.linux
要理解协程的实现, 首先须要了解go中的三个很是重要的概念, 它们分别是G, M和P,
没有看过golang源代码的可能会对它们感到陌生, 这三项是协程最主要的组成部分, 它们在golang的源代码中无处不在.git
G是goroutine的头文字, goroutine能够解释为受管理的轻量线程, goroutine使用go
关键词建立.程序员
举例来讲, func main() { go other() }
, 这段代码建立了两个goroutine,
一个是main, 另外一个是other, 注意main自己也是一个goroutine.github
goroutine的新建, 休眠, 恢复, 中止都受到go运行时的管理.
goroutine执行异步操做时会进入休眠状态, 待操做完成后再恢复, 无需占用系统线程,
goroutine新建或恢复时会添加到运行队列, 等待M取出并运行.golang
M是machine的头文字, 在当前版本的golang中等同于系统线程.
M能够运行两种代码:编程
M会从运行队列中取出G, 而后运行G, 若是G运行完毕或者进入休眠状态, 则从运行队列中取出下一个G运行, 周而复始.
有时候G须要调用一些没法避免阻塞的原生代码, 这时M会释放持有的P并进入阻塞状态, 其余M会取得这个P并继续运行队列中的G.
go须要保证有足够的M能够运行G, 不让CPU闲着, 也须要保证M的数量不能过多.bootstrap
P是process的头文字, 表明M运行G所须要的资源.
一些讲解协程的文章把P理解为cpu核心, 其实这是错误的.
虽然P的数量默认等于cpu核心数, 但能够经过环境变量GOMAXPROC
修改, 在实际运行时P跟cpu核心并没有任何关联.windows
P也能够理解为控制go代码的并行度的机制,
若是P的数量等于1, 表明当前最多只能有一个线程(M)执行go代码,
若是P的数量等于2, 表明当前最多只能有两个线程(M)执行go代码.
执行原生代码的线程数量不受P控制.数组
由于同一时间只有一个线程(M)能够拥有P, P中的数据都是锁自由(lock free)的, 读写这些数据的效率会很是的高.
在讲解协程的工做流程以前, 还须要理解一些内部的数据结构.
M并无像G和P同样的状态标记, 但能够认为一个M有如下的状态:
自旋中(spinning)这个状态很是重要, 是否须要唤醒或者建立新的M取决于当前自旋中的M的数量.
在go中有多个运行队列能够保存待运行(_Grunnable)的G, 它们分别是各个P中的本地运行队列和全局运行队列.
入队待运行的G时会优先加到当前P的本地运行队列, M获取待运行的G时也会优先从拥有的P的本地运行队列获取,
本地运行队列入队和出队不须要使用线程锁.
本地运行队列有数量限制, 当数量达到256个时会入队到全局运行队列.
本地运行队列的数据结构是环形队列, 由一个256长度的数组和两个序号(head, tail)组成.
当M从P的本地运行队列获取G时, 若是发现本地队列为空会尝试从其余P盗取一半的G过来,
这个机制叫作Work Stealing, 详见后面的代码分析.
全局运行队列保存在全局变量sched
中, 全局运行队列入队和出队须要使用线程锁.
全局运行队列的数据结构是链表, 由两个指针(head, tail)组成.
当M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 空闲M链表保存在全局变量sched
.
进入休眠的M会等待一个信号量(m.park), 唤醒休眠的M会使用这个信号量.
go须要保证有足够的M能够运行G, 是经过这样的机制实现的:
由于"入队待运行的G"和"M离开自旋状态"会同时进行, go会使用这样的检查顺序:
入队待运行的G => 内存屏障 => 检查当前自旋的M数量 => 唤醒或者新建一个M
减小当前自旋的M数量 => 内存屏障 => 检查全部运行队列是否有待运行的G => 休眠
这样能够保证不会出现待运行的G入队了, 也有空闲的资源P, 但无M去执行的状况.
当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 main import ( "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 main import ( "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.main hello`main.main: hello[0x401190] <+0>: movq %fs:-0x8, %rcx hello[0x401199] <+9>: cmpq 0x10(%rcx), %rsp hello[0x40119d] <+13>: jbe 0x401291 ; <+257> at hello.go:16 hello[0x4011a3] <+19>: subq $0x40, %rsp hello[0x4011a7] <+23>: leaq 0xb3632(%rip), %rbx ; runtime.rodata + 38880 hello[0x4011ae] <+30>: movq %rbx, (%rsp) hello[0x4011b2] <+34>: movq $0x3, 0x8(%rsp) hello[0x4011bb] <+43>: callq 0x4035a0 ; runtime.makechan at chan.go:49 hello[0x4011c0] <+48>: movq 0x10(%rsp), %rax hello[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.f hello[0x4011ef] <+95>: movq %rax, 0x8(%rsp) hello[0x4011f4] <+100>: callq 0x430cd0 ; runtime.newproc at proc.go:2657 hello[0x4011f9] <+105>: movq $0x4, 0x10(%rsp) hello[0x401202] <+114>: movq $0x6, 0x18(%rsp) hello[0x40120b] <+123>: movq 0x38(%rsp), %rbx hello[0x401210] <+128>: movq %rbx, 0x20(%rsp) hello[0x401215] <+133>: movl $0x18, (%rsp) hello[0x40121c] <+140>: leaq 0x129bf5(%rip), %rax ; main.printNumber.f hello[0x401223] <+147>: movq %rax, 0x8(%rsp) hello[0x401228] <+152>: callq 0x430cd0 ; runtime.newproc at proc.go:2657 hello[0x40122d] <+157>: movq $0x0, 0x30(%rsp) hello[0x401236] <+166>: leaq 0xb35a3(%rip), %rbx ; runtime.rodata + 38880 hello[0x40123d] <+173>: movq %rbx, (%rsp) hello[0x401241] <+177>: movq 0x38(%rsp), %rbx hello[0x401246] <+182>: movq %rbx, 0x8(%rsp) hello[0x40124b] <+187>: leaq 0x30(%rsp), %rbx hello[0x401250] <+192>: movq %rbx, 0x10(%rsp) hello[0x401255] <+197>: callq 0x4043c0 ; runtime.chanrecv1 at chan.go:354 hello[0x40125a] <+202>: movq $0x0, 0x28(%rsp) hello[0x401263] <+211>: leaq 0xb3576(%rip), %rbx ; runtime.rodata + 38880 hello[0x40126a] <+218>: movq %rbx, (%rsp) hello[0x40126e] <+222>: movq 0x38(%rsp), %rbx hello[0x401273] <+227>: movq %rbx, 0x8(%rsp) hello[0x401278] <+232>: leaq 0x28(%rsp), %rbx hello[0x40127d] <+237>: movq %rbx, 0x10(%rsp) hello[0x401282] <+242>: callq 0x4043c0 ; runtime.chanrecv1 at chan.go:354 hello[0x401287] <+247>: movq 0x28(%rsp), %rbx hello[0x40128c] <+252>: addq $0x40, %rsp hello[0x401290] <+256>: retq hello[0x401291] <+257>: callq 0x4538d0 ; runtime.morestack_noctxt at asm_amd64.s:365 hello[0x401296] <+262>: jmp 0x401190 ; <+0> at hello.go:16 hello[0x40129b] <+267>: int3 hello[0x40129c] <+268>: int3 hello[0x40129d] <+269>: int3 hello[0x40129e] <+270>: int3 hello[0x40129f] <+271>: int3 (lldb) di -n main.printNumber hello`main.printNumber: hello[0x401000] <+0>: movq %fs:-0x8, %rcx hello[0x401009] <+9>: leaq -0x8(%rsp), %rax hello[0x40100e] <+14>: cmpq 0x10(%rcx), %rax hello[0x401012] <+18>: jbe 0x401185 ; <+389> at hello.go:8 hello[0x401018] <+24>: subq $0x88, %rsp hello[0x40101f] <+31>: xorps %xmm0, %xmm0 hello[0x401022] <+34>: movups %xmm0, 0x60(%rsp) hello[0x401027] <+39>: movq 0x90(%rsp), %rax hello[0x40102f] <+47>: movq 0x98(%rsp), %rbp hello[0x401037] <+55>: cmpq %rbp, %rax hello[0x40103a] <+58>: jg 0x40112f ; <+303> at hello.go:13 hello[0x401040] <+64>: movq %rax, 0x40(%rsp) hello[0x401045] <+69>: movq %rax, 0x48(%rsp) hello[0x40104a] <+74>: xorl %ebx, %ebx hello[0x40104c] <+76>: movq %rbx, 0x60(%rsp) hello[0x401051] <+81>: movq %rbx, 0x68(%rsp) hello[0x401056] <+86>: leaq 0x60(%rsp), %rbx hello[0x40105b] <+91>: cmpq $0x0, %rbx hello[0x40105f] <+95>: je 0x40117e ; <+382> at hello.go:10 hello[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 + 54400 hello[0x401086] <+134>: movq %rbx, (%rsp) hello[0x40108a] <+138>: leaq 0x48(%rsp), %rbx hello[0x40108f] <+143>: movq %rbx, 0x8(%rsp) hello[0x401094] <+148>: movq $0x0, 0x10(%rsp) hello[0x40109d] <+157>: callq 0x40bb90 ; runtime.convT2E at iface.go:128 hello[0x4010a2] <+162>: movq 0x18(%rsp), %rcx hello[0x4010a7] <+167>: movq 0x20(%rsp), %rax hello[0x4010ac] <+172>: movq 0x70(%rsp), %rbx hello[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:10 hello[0x4010cb] <+203>: movq %rax, 0x8(%rbx) hello[0x4010cf] <+207>: leaq 0xfb152(%rip), %rbx ; go.string.* + 560 hello[0x4010d6] <+214>: movq %rbx, (%rsp) hello[0x4010da] <+218>: movq $0x3, 0x8(%rsp) hello[0x4010e3] <+227>: movq 0x70(%rsp), %rbx hello[0x4010e8] <+232>: movq %rbx, 0x10(%rsp) hello[0x4010ed] <+237>: movq 0x78(%rsp), %rbx hello[0x4010f2] <+242>: movq %rbx, 0x18(%rsp) hello[0x4010f7] <+247>: movq 0x80(%rsp), %rbx hello[0x4010ff] <+255>: movq %rbx, 0x20(%rsp) hello[0x401104] <+260>: callq 0x45ad70 ; fmt.Printf at print.go:196 hello[0x401109] <+265>: movq $0xf4240, (%rsp) ; imm = 0xF4240 hello[0x401111] <+273>: callq 0x442a50 ; time.Sleep at time.go:48 hello[0x401116] <+278>: movq 0x40(%rsp), %rax hello[0x40111b] <+283>: incq %rax hello[0x40111e] <+286>: movq 0x98(%rsp), %rbp hello[0x401126] <+294>: cmpq %rbp, %rax hello[0x401129] <+297>: jle 0x401040 ; <+64> at hello.go:10 hello[0x40112f] <+303>: movq $0x0, 0x48(%rsp) hello[0x401138] <+312>: leaq 0xb36a1(%rip), %rbx ; runtime.rodata + 38880 hello[0x40113f] <+319>: movq %rbx, (%rsp) hello[0x401143] <+323>: movq 0xa0(%rsp), %rbx hello[0x40114b] <+331>: movq %rbx, 0x8(%rsp) hello[0x401150] <+336>: leaq 0x48(%rsp), %rbx hello[0x401155] <+341>: movq %rbx, 0x10(%rsp) hello[0x40115a] <+346>: callq 0x403870 ; runtime.chansend1 at chan.go:99 hello[0x40115f] <+351>: addq $0x88, %rsp hello[0x401166] <+358>: retq hello[0x401167] <+359>: leaq 0x8(%rbx), %r8 hello[0x40116b] <+363>: movq %r8, (%rsp) hello[0x40116f] <+367>: movq %rax, 0x8(%rsp) hello[0x401174] <+372>: callq 0x40f090 ; runtime.writebarrierptr at mbarrier.go:129 hello[0x401179] <+377>: jmp 0x4010cf ; <+207> at hello.go:10 hello[0x40117e] <+382>: movl %eax, (%rbx) hello[0x401180] <+384>: jmp 0x401065 ; <+101> at hello.go:10 hello[0x401185] <+389>: callq 0x4538d0 ; runtime.morestack_noctxt at asm_amd64.s:365 hello[0x40118a] <+394>: jmp 0x401000 ; <+0> at hello.go:8 hello[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的全称是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, %rcx hello[0x401009] <+9>: leaq -0x8(%rsp), %rax hello[0x40100e] <+14>: cmpq 0x10(%rcx), %rax hello[0x401012] <+18>: jbe 0x401185 ; <+389> at hello.go:8
会检查比较rsp减去必定值之后是否比g.stackguard0小, 若是小于等于则须要调到下面调用morestack_noctxt函数.
细心的可能会发现比较的值跟实际减去的值不一致, 这是由于stackguard0下面会预留一小部分空间, 编译时肯定不超过预留的空间能够省略比对.
由于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也有写屏障的机制, 但做用跟这里的不同(用于标记跨代引用).
闭包这个概念自己应该不须要解释, 咱们实际看一看go是如何实现闭包的:
package main import ( "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等于1 hello[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+0x60 hello[0x4a099c] <+92>: movq %rax, (%rsp) ; 第一个参数等于地址rsp+0x60 hello[0x4a09a0] <+96>: callq 0x4a08f0 ; 执行main.executeFn hello[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+0x8 hello[0x4a0908] <+24>: leaq 0x8(%rsp), %rbp ; 把变量rsp+0x8的地址移到寄存器rbp hello[0x4a090d] <+29>: movq 0x18(%rsp), %rdx ; 把第一个参数(闭包)的指针移到寄存器rdx hello[0x4a0912] <+34>: movq (%rdx), %rax ; 把闭包中函数的指针移到寄存器rax hello[0x4a0915] <+37>: callq *%rax ; 调用闭包中的函数 hello[0x4a0917] <+39>: movq (%rsp), %rax ; 把返回值移到寄存器rax hello[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 ; 从函数返回
闭包的传递能够总结以下:
细心的可能会发如今上面的例子中, 闭包的内容在栈上, 若是不是直接调用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.newobject hello[0x455621] <+49>: movq 0x8(%rsp), %rax ; 寄存器rax等于返回值(这里称为新对象a) hello[0x455626] <+54>: movq %rax, 0x28(%rsp) ; 变量rsp+0x28等于新对象a hello[0x45562b] <+59>: movq $0x1, (%rax) ; 新对象a的值等于1 hello[0x455632] <+66>: leaq 0x136e7(%rip), %rcx ; 寄存器rcx等于类型信息 hello[0x455639] <+73>: movq %rcx, (%rsp) ; 第一个参数等于类型信息 hello[0x45563d] <+77>: callq 0x40d910 ; 调用runtime.newobject hello[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不等于nil hello[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等于新对象a hello[0x455666] <+118>: movq %rcx, 0x8(%rax) ; 设置新对象fn+0x8的值等于新对象a hello[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), 而后传入函数+参数, 上面的参数即闭包的地址.
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, 流程是:
调用runtime.osinit根据系统执行不一样的初始化
调用runtime.schedinit执行共同的初始化
调用runtime.newproc建立一个新的goroutine, 指向的是runtime.main
调用runtime·mstart启动m0
第一个被调度的G会运行runtime.main, 流程是:
G里面比较重要的成员以下
M里面比较重要的成员以下
P里面比较重要的成员以下
使用go命令建立goroutine时, go会把go命令编译为对runtime.newproc的调用, 堆栈的结构以下:
第一个参数是funcval + 额外参数的长度, 第二个参数是funcval, 后面的都是传递给goroutine中执行的函数的额外参数.
funcval的定义在这里, fn是指向函数机器代码的指针.
runtime.newproc的处理以下:
systemstack会切换当前的g到g0, 而且使用g0的栈空间, 而后调用传入的函数, 再切换回原来的g和原来的栈空间.
切换到g0后会伪装返回地址是mstart, 这样traceback的时候能够在mstart中止.
这里传给systemstack的是一个闭包, 调用时会把闭包的地址放到寄存器rdx, 具体能够参考上面对闭包的分析.
runtime.newproc1的处理以下:
新建一个g
设置g的调度数据(sched)
调用runqput把g放到运行队列
若是本地运行队列满了则调用runqputslow把g放到"全局运行队列"
若是当前有空闲的P, 可是无自旋的M(nmspinning等于0), 而且主函数已执行则唤醒或新建一个M
唤醒或新建一个M会经过wakep函数
调用startm函数
若是没有空闲的M, 则调用newm新建一个M
建立goroutine的流程就这么多了, 接下来看看M是如何调度的.
M启动时会调用mstart函数, m0在初始化后调用, 其余的的m在线程启动后调用.
mstart函数的处理以下:
调用mstart1函数
调用schedule函数后就进入了调度循环, 整个流程能够简单总结为:
schedule函数获取g => [必要时休眠] => [唤醒后继续获取] => execute函数执行g => 执行后返回到goexit => 从新执行schedule函数
schedule函数的处理以下:
快速获取待运行的G, 如下处理若是有一个获取成功后面就不会继续获取
快速获取失败时, 调用findrunnable函数获取待运行的G, 会阻塞到获取成功为止
若是获取不到G, 则执行Work Stealing
若是仍是获取不到G, 就须要休眠M了, 接下来是休眠的步骤
让M离开自旋状态, 调用resetspinning, 这里的处理和上面的不同
若是G要求回到指定的M(例如上面的runtime.main)
execute函数的处理以下:
调用gogo函数
g.sched.pc在G首次运行时会指向目标函数的第一条机器指令,
若是G被抢占或者等待资源而进入休眠, 在休眠前会保存状态到g.sched,
g.sched.pc会变为唤醒后须要继续执行的地址, "保存状态"的实现将在下面讲解.
目标函数执行完毕后会调用goexit函数, goexit函数会调用goexit1函数, goexit1函数会经过mcall调用goexit0函数.
mcall这个函数就是用于实现"保存状态"的, 处理以下:
mcall这个函数保存当前的运行状态到g.sched, 而后切换到g0和g0的栈空间, 再调用指定的函数.
回到g0的栈空间这个步骤很是重要, 由于这个时候g已经中断, 继续使用g的栈空间且其余M唤醒了这个g将会产生灾难性的后果.
G在中断或者结束后都会经过mcall回到g0的栈空间继续调度, 从goexit调用的mcall的保存状态实际上是多余的, 由于G已经结束了.
goexit1函数会经过mcall调用goexit0函数, goexit0函数调用时已经回到了g0的栈空间, 处理以下:
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
若是P在运行中(_Prunning), 且通过了一次sysmon循环而且G运行时间超过forcePreemptNS(10ms), 则抢占这个P
调用preemptone函数
为何设置了stackguard就能够实现抢占?
由于这个值用于检查当前栈空间是否足够, go函数的开头会比对这个值判断是否须要扩张栈.
stackPreempt是一个特殊的常量, 它的值会比任何的栈地址都要大, 检查时必定会触发栈扩张.
栈扩张调用的是morestack_noctxt函数, morestack_noctxt函数清空rdx寄存器并调用morestack函数.
morestack函数会保存G的状态到g.sched, 切换到g0和g0的栈空间, 而后调用newstack函数.
newstack函数判断g.stackguard0等于stackPreempt, 就知道这是抢占触发的, 这时会再检查一遍是否要抢占:
即便这一次抢占失败, 由于g.preempt等于true, runtime中的一些代码会从新设置stackPreempt以重试下一次的抢占.
若是判断能够抢占, 则继续判断是否GC引发的, 若是是则对G的栈空间执行标记处理(扫描根对象)而后继续运行,
若是不是GC引发的则调用gopreempt_m函数完成抢占.
gopreempt_m函数会调用goschedImpl函数, goschedImpl函数的流程是:
由于全局运行队列的优先度比较低, 各个M会通过一段时间再去从新获取这个G执行,
抢占机制保证了不会有一个G长时间的运行致使其余G没法运行的状况发生.
在goroutine运行的过程当中, 有时候须要对资源进行等待, channel就是最典型的资源.
channel的数据定义在这里, 其中关键的成员以下:
发送数据到channel实际调用的是runtime.chansend1函数, chansend1函数调用了chansend函数, 流程是:
检查channel.recvq是否有等待中的接收者的G
调用send函数
_
则elem是nil, 能够省略复制复制后调用goready恢复发送者的G
切换到g0调用ready函数, 调用完切换回来
判断是否能够把元素放到缓冲区中
无缓冲区或缓冲区已经写满, 发送者的G须要等待
调用goparkunlock函数
从这里恢复表示已经成功发送或者channel已关闭
从channel接收数据实际调用的是runtime.chanrecv1函数, chanrecv1函数调用了chanrecv函数, 流程是:
检查channel.sendq中是否有等待中的发送者的G
调用recv函数
若是有缓冲区表明缓冲区已满
判断是否能够从缓冲区获取元素
无缓冲区或缓冲区无元素, 接收者的G须要等待
从这里恢复表示已经成功接收或者channel已关闭
关闭channel实际调用的是closechan函数, 流程是:
能够看到若是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工程师的功力, 能够写出这样简单易懂的代码不容易.