GO GMP协程调度实现原理 5w字长文史上最全
1 Runtime简介


1 buf := &getg().m.p.ptr().wbBuf
2 GMP概览
1 +-------------------- sysmon ---------------//------+
2 | |
3 | |
4 +---+ +---+-------+ +--------+ +---+---+
5 go func() ---> | G | ---> | P | local | <=== balance ===> | global | <--//--- | P | M |
6 +---+ +---+-------+ +--------+ +---+---+
7 | | |
8 | +---+ | |
9 +----> | M | <--- findrunnable ---+--- steal <--//--+
10 +---+
11 |
12 mstart
13 |
14 +--- execute <----- schedule
15 | |
16 | |
17 +--> G.fn --> goexit --+
- G:Groutine协程,拥有运行函数的指针、栈、上下文(指的是sp、bp、pc等寄存器上下文以及垃圾回收的标记上下文),在整个程序运行过程中可以有无数个,代表一个用户级代码执行流(用户轻量级线程);
- P:Processor,调度逻辑处理器,同样也是Go中代表资源的分配主体(内存资源、协程队列等),默认为机器核数,可以通过GOMAXPROCS环境变量调整
- M:Machine,代表实际工作的执行者,对应到操作系统级别的线程;M的数量会比P多,但不会太多,最大为1w个。
- 主协程,用来执行用户main函数的协程
- 主协程创建的协程,也是P调度的主要成员
- G0,每个M都有一个G0协程,他是runtime的一部分,G0是跟M绑定的,主要用来执行调度逻辑的代码,所以不能被抢占也不会被调度(普通G也可以执行runtime_procPin禁止抢占),G0的栈是系统分配的,比普通的G栈(2KB)要大,不能扩容也不能缩容
- sysmon协程,sysmon协程也是runtime的一部分,sysmon协程直接运行在M不需要P,主要做一些检查工作如:检查死锁、检查计时器获取下一个要被触发的计时任务、检查是否有ready的网络调用以恢复用户G的工作、检查一个G是否运行时间太长进行抢占式调度。
- 普通M,用来与P绑定执行G中任务
- m0:Go程序是一个进程,进程都有一个主线程,m0就是Go程序的主线程,通过一个与其绑定的G0来执行runtime启动加载代码;一个Go程序只有一个m0
- 运行sysmon的M,主要用来运行sysmon协程。
- 主动调度,协程通过调用`runtime.Goshed`方法主动让渡自己的执行权利,之后这个协程会被放到全局队列中,等待后续被执行
- 被动调度,协程在休眠、channel通道阻塞、网络I/O堵塞、执行垃圾回收时被暂停,被动式让渡自己的执行权利。大部分场景都是被动调度,这是Go高性能的一个原因,让M永远不停歇,不处于等待的协程让出CPU资源执行其他任务。
- 抢占式调度,这个主要是sysmon协程上的调度,当发现G处于系统调用(如调用网络io)超过20微秒或者G运行时间过长(超过10ms),会抢占G的执行CPU资源,让渡给其他协程;防止其他协程没有执行的机会;(系统调用会进入内核态,由内核线程完成,可以把当前CPU资源让渡给其他用户协程)
- 调度发生地点:Go中协程的调度发生在runtime,属于用户态,不涉及与内核态的切换;一个协程可以被切换到多个线程执行
- 上下文切换速度:协程的切换速度远快于线程,不需要经过内核与用户态切换,同时需要保存的状态和寄存器非常少;线程切换速度为1-2微秒,协程切换速度为0.2微秒左右
- 调度策略:线程调度大部分都是抢占式调度,操作系统通过发出中断信号强制线程切换上下文;Go的协程基本是主动和被动式调度,调度时机可预期
- 栈大小:线程栈一般是2MB,而且运行时不能更改大小;Go的协程栈只有2kb,而且可以动态扩容(64位机最大为1G)
3 GMP的源码结构
1 runtime/amd_64.s 涉及到进程启动以及对CPU执行指令进行控制的汇编代码,进程的初始化部分也在这里面
2 runtime/runtime2.go 这里主要是运行时中一些重要数据结构的定义,比如g、m、p以及涉及到接口、defer、panic、map、slice等核心类型
3 runtime/proc.go 一些核心方法的实现,涉及gmp调度等核心代码在这里
3.1 G源码部分
3.1.1 G的结构
1 // runtime/runtime2.go
2 type g struct {
3 // 记录协程栈的栈顶和栈底位置
4 stack stack // offset known to runtime/cgo
5 // 主要作用是参与一些比较计算,当发现容量要超过栈分配空间后,可以进行扩容或者收缩
6 stackguard0 uintptr // offset known to liblink
7 stackguard1 uintptr // offset known to liblink
8
9 // 当前与g绑定的m
10 m *m // current m; offset known to arm liblink
11 // 这是一个比较重要的字段,里面保存的一些与goroutine运行位置相关的寄存器和指针,如rsp、rbp、rpc等寄存器
12 sched gobuf
13 syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
14 syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
15 stktopsp uintptr // expected sp at top of stack, to check in traceback
16
17 // 用于做参数传递,睡眠时其他goroutine可以设置param,唤醒时该g可以读取这些param
18 param unsafe.Pointer
19 // 记录当前goroutine的状态
20 atomicstatus uint32
21 stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
22 // goroutine的唯一id
23 goid int64
24 schedlink guintptr
25
26 // 标记是否可以被抢占
27 preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
28 preemptStop bool // transition to _Gpreempted on preemption; otherwise, just deschedule
29 preemptShrink bool // shrink stack at synchronous safe point
30
31 // 如果调用了LockOsThread方法,则g会绑定到某个m上,只在这个m上运行
32 lockedm muintptr
33 sig uint32
34 writebuf []byte
35 sigcode0 uintptr
36 sigcode1 uintptr
37 sigpc uintptr
38 // 创建该goroutine的语句的指令地址
39 gopc uintptr // pc of go statement that created this goroutine
40 ancestors *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors)
41 // goroutine函数的指令地址
42 startpc uintptr // pc of goroutine function
43 racectx uintptr
44 waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
45 cgoCtxt []uintptr // cgo traceback context
46 labels unsafe.Pointer // profiler labels
47 timer *timer // cached timer for time.Sleep
48 selectDone uint32 // are we participating in a select and did someone win the race?
49 }
- SP:永远指向栈顶位置
- BP:某一时刻的栈顶位置,当新函数调用时,把当前SP地址赋值给BP、SP指向新的栈顶位置
- PC:代表代码经过编译为机器码后,当前执行的机器指令(可以理解为当前语句)
1 // Stack describes a Go execution stack.
2 // The bounds of the stack are exactly [lo, hi),
3 // with no implicit data structures on either side.
4 // goroutine协程栈的栈顶和栈底
5 type stack struct {
6 lo uintptr // 栈顶,低地址
7 hi uintptr // 栈底,高地址
8 }
9
10 // gobuf中保存了非常重要的上下文执行信息,
11 type gobuf struct {
12 // 代表cpu的rsp寄存器的值,永远指向栈顶位置
13 sp uintptr
14 // 代表代码经过编译为机器码后,当前执行的机器指令(可以理解为当前语句)
15 pc uintptr
16 // 指向所保存执行上下文的goroutine
17 g guintptr
18 // gc时候使用
19 ctxt unsafe.Pointer
20 // 用来保存系统调用的返回值
21 ret uintptr
22 lr uintptr
23 // 某一时刻的栈顶位置,当新函数调用时,把当前SP地址赋值给BP、SP指向新的栈顶位置
24 bp uintptr // for framepointer-enabled architectures
25 }
3.1.2 G的状态
1 // defined constants
2 const (
3 // 代表协程刚开始创建时的状态,当新创建的协程初始化后,为变为_Gdead状态,_Gdread也是协程被销毁时的状态;
4 // 刚创建时也被会置为_Gdead主要是考虑GC可以去用去扫描dead状态下的协程栈
5 _Gidle = iota // 0
6 // 代表协程正在运行队列中,等待被运行
7 _Grunnable // 1
8 // 代表当前协程正在被运行,已经被分配了逻辑处理的线程,即p和m
9 _Grunning // 2
10 // 代表当前协程正在执行系统调用
11 _Gsyscall // 3
12 // 表示当前协程在运行时被锁定,陷入阻塞,不能执行用户代码
13 _Gwaiting // 4
14
15 _Gmoribund_unused // 5
16 // 新创建的协程初始化后,或者协程被销毁后的状态
17 _Gdead // 6
18
19 // _Genqueue_unused is currently unused.
20 _Genqueue_unused // 7
21 // 代表在进行协程栈扫描时发现需要扩容或者缩容,将协程中的栈转移到新栈时的状态;这个时候不执行用户代码,也不在p的runq中
22 _Gcopystack // 8
23
24 // 代表g被抢占后的状态
25 _Gpreempted // 9
26
27 // 这几个状态是垃圾回收时涉及,后续文章进行介绍
28 _Gscan = 0x1000
29 _Gscanrunnable = _Gscan + _Grunnable // 0x1001
30 _Gscanrunning = _Gscan + _Grunning // 0x1002
31 _Gscansyscall = _Gscan + _Gsyscall // 0x1003
32 _Gscanwaiting = _Gscan + _Gwaiting // 0x1004
33 _Gscanpreempted = _Gscan + _Gpreempted // 0x1009
34 )

3.1.3 G的创建
1 func newproc(siz int32, fn *funcval) {
2 argp := add(unsafe.Pointer(&fn), sys.PtrSize)
3 gp := getg()
4 // 获取调用者的指令地址,也就是调用newproc时又call指令压栈的函数返回地址
5 pc := getcallerpc()
6 // systemstack的作用是切换到m0对应的g0所属的系统栈
7 // 使用g0所在的系统栈创建goroutine对象
8 // 传递参数包括goroutine的任务函数、argp参数起始地址、siz是参数长度、调用方的pc指针
9 systemstack(func() {
10 newg := newproc1(fn, argp, siz, gp, pc)
11 // 创建完成后将g放到创建者(某个g,如果是进程初始化启动阶段则为g0)所在的p的队列中
12 _p_ := getg().m.p.ptr()
13 runqput(_p_, newg, true)
14
15 if mainStarted {
16 wakep()
17 }
18 })
19 }
1 func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
2 .....
3 // 如果是初始化时候这个代表g0
4 _g_ := getg()
5
6 if fn == nil {
7 _g_.m.throwing = -1 // do not dump full stacks
8 throw("go of nil func value")
9 }
10 // 使_g_.m.locks++,来防止这个时候g对应的m被抢占
11 acquirem() // disable preemption because it can be holding p in a local var
12 // 参数的地址,下面一句目的是为了做到内存对齐
13 siz := narg
14 siz = (siz + 7) &^ 7
15
16 // We could allocate a larger initial stack if necessary.
17 // Not worth it: this is almost always an error.
18 // 4*PtrSize: extra space added below
19 // PtrSize: caller's LR (arm) or return address (x86, in gostartcall).
20 if siz >= _StackMin-4*sys.PtrSize-sys.PtrSize {
21 throw("newproc: function arguments too large for new goroutine")
22 }
23
24 _p_ := _g_.m.p.ptr()
25 newg := gfget(_p_) // 首先从p的gfree队列中看看有没有空闲的g,有则使用
26 if newg == nil {
27 // 如果没找到就使用new关键字来创建一个g并在堆上分配栈空间
28 newg = malg(_StackMin)
29 // 将newg的状态设置为_Gdead,因为这样GC就不会去扫描一个没有初始化的协程栈
30 casgstatus(newg, _Gidle, _Gdead)
31 // 添加到全局的allg切片中(需要加锁访问)
32 allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
33 }
34 // 下面是检查协程栈的创建情况和状态
35 if newg.stack.hi == 0 {
36 throw("newproc1: newg missing stack")
37 }
38
39 if readgstatus(newg) != _Gdead {
40 throw("newproc1: new g is not Gdead")
41 }
42 // 计算运行空间大小并进行内存对齐
43 totalSize := 4*sys.PtrSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
44 totalSize += -totalSize & (sys.StackAlign - 1) // align to StackAlign
45 // 计算sp寄存器指针的位置
46 sp := newg.stack.hi - totalSize
47 // 确定参数入栈位置
48 spArg := sp
49 .........
50 if narg > 0 {
51 // 将参数从newproc函数的栈复制到新的协程的栈中,memove是一段汇编代码
52 // 从argp位置挪动narg大小的内存到sparg位置
53 memmove(unsafe.Pointer(spArg), argp, uintptr(narg))
54 // 因为涉及到从栈到堆栈上的复制,go在垃圾回收中使用了三色标记和写入屏障等手段,所以这里要考虑屏障复制
55 // 目标栈可能会有垃圾存在,所以设置屏障并且标记为灰色
56 if writeBarrier.needed && !_g_.m.curg.gcscandone { // 如果启用了写入屏障并且源堆栈为灰色(目标始终为黑色),则执行屏障复制。
57 f := findfunc(fn.fn)
58 stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
59 if stkmap.nbit > 0 {
60 // We're in the prologue, so it's always stack map index 0.
61 bv := stackmapdata(stkmap, 0)
62 bulkBarrierBitmap(spArg, spArg, uintptr(bv.n)*sys.PtrSize, 0, bv.bytedata)
63 }
64 }
65 }
66 // 把newg的sched结构体成员的所有字段都设置为0,其实就是初始化
67 memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
68 newg.sched.sp = sp
69 newg.stktopsp = sp
70 // pc指针表示当newg被调度起来时从这个位置开始执行
71 // 这里是先设置为goexit,在gostartcallfn中会进行处理,更改sp为这里的pc,将pc改为真正的协程任务函数fn的指令位置
72 // 这样使得任务函数执行完毕后,会继续执行goexit中相关的清理工作
73 newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
74 newg.sched.g = guintptr(unsafe.Pointer(newg)) // 保存当前的g
75 gostartcallfn(&newg.sched, fn) // 在这里完成g启动时所有相关上下文指针的设置,主要为sp、pc和ctxt,ctxt被设置为fn
76 newg.gopc = callerpc // 保存newproc的pc,即调用者创建时的指令位置
77 newg.ancestors = saveAncestors(callergp)
78 // 设置startpc为任务函数,主要用于函数调用栈的trackback和栈收缩工作
79 // newg的执行开始位置并不依赖这个字段,而是通过sched.pc确定
80 newg.startpc = fn.fn
81 if _g_.m.curg != nil {
82 newg.labels = _g_.m.curg.labels
83 }
84 // 判断newg的任务函数是不是runtime系统的任务函数,是则sched.ngsys+1;
85 // 主协程则代表runtime.main函数,在这里就为判断为真
86 if isSystemGoroutine(newg, false) {
87 atomic.Xadd(&sched.ngsys, +1)
88 }
89 // Track initial transition?
90 newg.trackingSeq = uint8(fastrand())
91 if newg.trackingSeq%gTrackingPeriod == 0 {
92 newg.tracking = true
93 }
94 // 更改当前g的状态为_Grunnable
95 casgstatus(newg, _Gdead, _Grunnable)
96 // 设置g的goid,因为p会每次批量生成16个id,每次newproc如果新建一个g,id就会加1
97 // 所以这里m0的g0的id为0,而主协程的goid为1,其他的依次递增
98 if _p_.goidcache == _p_.goidcacheend {
99 // Sched.goidgen is the last allocated id,
100 // this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
101 // At startup sched.goidgen=0, so main goroutine receives goid=1.
102 // 使用原子操作修改全局变量,这里的sched是在runtime2.go中的一个全局变量类型为schedt
103 // 原子操作具有多线程可见性,同时比加锁性能更高
104 _p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
105 _p_.goidcache -= _GoidCacheBatch - 1
106 _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
107 }
108 newg.goid = int64(_p_.goidcache)
109 _p_.goidcache++
110 if raceenabled {
111 newg.racectx = racegostart(callerpc)
112 }
113 if trace.enabled {
114 traceGoCreate(newg, newg.startpc)
115 }
116 // 释放getg与m的绑定
117 releasem(_g_.m)
118
119 return newg
120 }
3.1.4 协程栈在堆空间的分配
1 // Allocate a new g, with a stack big enough for stacksize bytes.
2 func malg(stacksize int32) *g {
3 newg := new(g)
4 if stacksize >= 0 {
5 stacksize = round2(_StackSystem + stacksize)
6 systemstack(func() {
7 newg.stack = stackalloc(uint32(stacksize))
8 })
9 newg.stackguard0 = newg.stack.lo + _StackGuard
10 newg.stackguard1 = ^uintptr(0)
11 // Clear the bottom word of the stack. We record g
12 // there on gsignal stack during VDSO on ARM and ARM64.
13 *(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0
14 }
15 return newg
16 }
1 OS | FixedStack | NumStackOrders
2 -----------------+------------+---------------
3 linux/darwin/bsd | 2KB | 4
4 windows/32 | 4KB | 3
5 windows/64 | 8KB | 2
6 plan9 | 4KB | 3
7
8 // Global pool of spans that have free stacks.
9 // Stacks are assigned an order according to size.
10 // order = log_2(size/FixedStack)
11 // There is a free list for each order.
12 var stackpool [_NumStackOrders]struct {
13 item stackpoolItem
14 _ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte
15 }
16
17 //go:notinheap
18 type stackpoolItem struct {
19 mu mutex
20 span mSpanList
21 }
22
23 // Global pool of large stack spans.
24 var stackLarge struct {
25 lock mutex
26 free [heapAddrBits - pageShift]mSpanList // free lists by log_2(s.npages)
27 }
28
29 func stackinit() {
30 if _StackCacheSize&_PageMask != 0 {
31 throw("cache size must be a multiple of page size")
32 }
33 for i := range stackpool {
34 stackpool[i].item.span.init()
35 lockInit(&stackpool[i].item.mu, lockRankStackpool)
36 }
37 for i := range stackLarge.free {
38 stackLarge.free[i].init()
39 lockInit(&stackLarge.lock, lockRankStackLarge)
40 }
41 }
1 func stackalloc(n uint32) stack {
2 // Stackalloc must be called on scheduler stack, so that we
3 // never try to grow the stack during the code that stackalloc runs.
4 // Doing so would cause a deadlock (issue 1547).
5 thisg := getg()
6 .........
7
8 // Small stacks are allocated with a fixed-size free-list allocator.
9 // If we need a stack of a bigger size, we fall back on allocating
10 // a dedicated span.
11 var v unsafe.Pointer
12 if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
13 order := uint8(0)
14 n2 := n
15 for n2 > _FixedStack {
16 order++
17 n2 >>= 1
18 }
19 var x gclinkptr
20 if stackNoCache != 0 || thisg.m.p == 0 || thisg.m.preemptoff != "" {
21 // thisg.m.p == 0 can happen in the guts of exitsyscall
22 // or procresize. Just get a stack from the global pool.
23 // Also don't touch stackcache during gc
24 // as it's flushed concurrently.
25 lock(&stackpool[order].item.mu)
26 x = stackpoolalloc(order)
27 unlock(&stackpool[order].item.mu)
28 } else {
29 c := thisg.m.p.ptr().mcache
30 x = c.stackcache[order].list
31 if x.ptr() == nil {
32 stackcacherefill(c, order)
33 x = c.stackcache[order].list
34 }
35 c.stackcache[order].list = x.ptr().next
36 c.stackcache[order].size -= uintptr(n)
37 }
38 v = unsafe.Pointer(x)
39 } else {
40 var s *mspan
41 npage := uintptr(n) >> _PageShift
42 log2npage := stacklog2(npage)
43
44 // Try to get a stack from the large stack cache.
45 lock(&stackLarge.lock)
46 if !stackLarge.free[log2npage].isEmpty() {
47 s = stackLarge.free[log2npage].first
48 stackLarge.free[log2npage].remove(s)
49 }
50 unlock(&stackLarge.lock)
51
52 lockWithRankMayAcquire(&mheap_.lock, lockRankMheap)
53
54 if s == nil {
55 // Allocate a new stack from the heap.
56 s = mheap_.allocManual(npage, spanAllocStack)
57 if s == nil {
58 throw("out of memory")
59 }
60 osStackAlloc(s)
61 s.elemsize = uintptr(n)
62 }
63 v = unsafe.Pointer(s.base())
64 }
65
66 if raceenabled {
67 racemalloc(v, uintptr(n))
68 }
69 if msanenabled {
70 msanmalloc(v, uintptr(n))
71 }
72 if stackDebug >= 1 {
73 print(" allocated ", v, "\n")
74 }
75 return stack{uintptr(v), uintptr(v) + uintptr(n)}
76 }
3.1.5 G的上下文设置和切换
1 // 把newg的sched结构体成员的所有字段都设置为0,其实就是初始化
2 memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
3 newg.sched.sp = sp
4 newg.stktopsp = sp
5 // pc指针表示当newg被调度起来时从这个位置开始执行
6 // 这里是先设置为goexit,在gostartcallfn中会进行处理,更改sp为这里的pc,将pc改为真正的协程任务函数fn的指令位置
7 // 这样使得任务函数执行完毕后,会继续执行goexit中相关的清理工作
8 newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
9 newg.sched.g = guintptr(unsafe.Pointer(newg)) // 保存当前的g
10 gostartcallfn(&newg.sched, fn) // 在这里完成g启动时所有相关上下文指针的设置,主要为sp、pc和ctxt,ctxt被设置为fn
11 newg.gopc = callerpc // 保存newproc的pc,即调用者创建时的指令位置
1 // gostartcallfn 位于runtime/stack.go中
2
3 func gostartcallfn(gobuf *gobuf, fv *funcval) {
4 var fn unsafe.Pointer
5 if fv != nil {
6 fn = unsafe.Pointer(fv.fn)
7 } else {
8 fn = unsafe.Pointer(funcPC(nilfunc))
9 }
10 gostartcall(gobuf, fn, unsafe.Pointer(fv))
11 }
12
13 // gostartcall 位于runtime/sys_x86.go中
14
15 func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
16 // newg的栈顶,目前newg栈上只有fn函数的参数,sp指向的是fn的第一个参数
17 sp := buf.sp
18 // 为返回地址预留空间
19 sp -= sys.PtrSize
20 // buf.pc中设置的是goexit函数中的第二条指令
21 // 因为栈是自顶向下,先进后出,所以这里伪装fn是被goexit函数调用的,goexit在前fn在后
22 // 使得fn返回后到goexit继续执行,以完成一些清理工作。
23 *(*uintptr)(unsafe.Pointer(sp)) = buf.pc
24 buf.sp = sp // 重新设置栈顶
25 // 将pc指向goroutine的任务函数fn,这样当goroutine获得执行权时,从任务函数入口开始执行
26 // 如果是主协程,那么fn就是runtime.main,从这里开始执行
27 buf.pc = uintptr(fn)
28 buf.ctxt = ctxt
29 }
1 // gogo的具体汇编代码位于asm_amd64.s中
2
3 // func gogo(buf *gobuf)
4 // restore state from Gobuf; longjmp
5 TEXT runtime·gogo(SB), NOSPLIT, $0-8
6 // 0(FP)表示第一个参数,即buf=&gp.sched
7 MOVQ buf+0(FP), BX // gobuf
8 // DX = gp.sched.g,DX代表数据寄存器
9 MOVQ gobuf_g(BX), DX
10 MOVQ 0(DX), CX // make sure g != nil
11 JMP gogo<>(SB)
12
13 TEXT gogo<>(SB), NOSPLIT, $0
14 // 将tls保存到CX寄存器
15 get_tls(CX)
16 // 下面这条指令把当前要运行的g(上面第9行中已经把go_buf中的g放入到了DX中),
17 // 放入CX寄存器的g位置即tls[0]这个位置,也就是线程的本地存储中,
18 // 这样下次runtime中调用getg时获取的就是这个g
19 MOVQ DX, g(CX)
20 MOVQ DX, R14 // set the g register
21 // 把CPU的SP寄存器设置为g.sched.sp这样就完成了栈的切换,从g0切换为g
22 MOVQ gobuf_sp(BX), SP // restore SP
23 // 将ret、ctxt、bp分别存入对应的寄存器,完成了CPU上下文的切换
24 MOVQ gobuf_ret(BX), AX
25 MOVQ gobuf_ctxt(BX), DX
26 MOVQ gobuf_bp(BX), BP
27 // 清空sched的值,相关值已经存入到寄存器中,这里清空后可以减少GC的工作量
28 MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
29 MOVQ $0, gobuf_ret(BX)
30 MOVQ $0, gobuf_ctxt(BX)
31 MOVQ $0, gobuf_bp(BX)
32 // 把sched.pc放入BX寄存器
33 MOVQ gobuf_pc(BX), BX
34 // JMP把BX的值放入CPU的IP寄存器,所以这时候CPU从该地址开始继续执行指令
35 JMP BX
3.1.6 G的退出处理
1 // runtime/proc.go 中的main函数
2 // The main goroutine.
3 func main() {
4 g := getg()
5
6 .................
7 fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
8 fn()
9 ..................
10 ..................
11 exit(0)
12 ..................
13 }
1 // The top-most function running on a goroutine
2 // returns to goexit+PCQuantum.
3 TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
4 BYTE $0x90 // NOP
5 CALL runtime·goexit1(SB) // does not return
6 // traceback from goexit1 must hit code range of goexit
7 BYTE $0x90 // NOP
1 // Finishes execution of the current goroutine.
2 func goexit1() {
3 if raceenabled {
4 racegoend()
5 }
6 if trace.enabled {
7 traceGoEnd()
8 }
9 mcall(goexit0)
10 }
1 TEXT runtime·mcall<ABIInternal>(SB), NOSPLIT, $0-8
2 MOVQ AX, DX // DX = fn
3
4 // save state in g->sched
5 // mcall返回地址放入BX中
6 MOVQ 0(SP), BX // caller's PC
7 // 下面部分是保存g的执行上下文,pc、sp、bp
8 // g.shced.pc = BX
9 MOVQ BX, (g_sched+gobuf_pc)(R14)
10 LEAQ fn+0(FP), BX // caller's SP
11 MOVQ BX, (g_sched+gobuf_sp)(R14)
12 MOVQ BP, (g_sched+gobuf_bp)(R14)
13
14 // switch to m->g0 & its stack, call fn
15 // 将g.m保存到BX寄存器中
16 MOVQ g_m(R14), BX
17 // 这段代码意思是从m结构体中获取g0字段保存到SI中
18 MOVQ m_g0(BX), SI // SI = g.m.g0
19 CMPQ SI, R14 // if g == m->g0 call badmcall
20 // goodm中完成了从g的栈切换到g0的栈
21 JNE goodm
22 JMP runtime·badmcall(SB)
23 goodm:
24 MOVQ R14, AX // AX (and arg 0) = g
25 MOVQ SI, R14 // g = g.m.g0
26 get_tls(CX) // Set G in TLS
27 MOVQ R14, g(CX)
28 MOVQ (g_sched+gobuf_sp)(R14), SP // sp = g0.sched.sp
29 PUSHQ AX // open up space for fn's arg spill slot
30 MOVQ 0(DX), R12
31 // 这里意思是调用goexit0(g)
32 CALL R12 // fn(g)
33 POPQ AX
34 JMP runtime·badmcall2(SB)
35 RET
1 // 这段代码执行在g0的栈上,gp是我们要处理退出的g的结构体指针
2 // goexit continuation on g0.
3 func goexit0(gp *g) {
4 _g_ := getg() // 获取g0
5 // 更改g的状态为_Gdead
6 casgstatus(gp, _Grunning, _Gdead)
7 if isSystemGoroutine(gp, false) {
8 atomic.Xadd(&sched.ngsys, -1)
9 }
10 // 清空g的一些字段
11 gp.m = nil
12 locked := gp.lockedm != 0
13 gp.lockedm = 0
14 _g_.m.lockedg = 0
15 gp.preemptStop = false
16 gp.paniconfault = false
17 gp._defer = nil // should be true already but just in case.
18 gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
19 gp.writebuf = nil
20 gp.waitreason = 0
21 gp.param = nil
22 gp.labels = nil
23 gp.timer = nil
24
25 if gcBlackenEnabled != 0 && gp.gcAssistBytes > 0 {
26 // Flush assist credit to the global pool. This gives
27 // better information to pacing if the application is
28 // rapidly creating an exiting goroutines.
29 assistWorkPerByte := float64frombits(atomic.Load64(&gcController.assistWorkPerByte))
30 scanCredit := int64(assistWorkPerByte * float64(gp.gcAssistBytes))
31 atomic.Xaddint64(&gcController.bgScanCredit, scanCredit)
32 gp.gcAssistBytes = 0
33 }
34 // 接触g与m的绑定关系
35 dropg()
36
37 if GOARCH == "wasm" { // no threads yet on wasm
38 gfput(_g_.m.p.ptr(), gp)
39 schedule() // never returns
40 }
41
42 if _g_.m.lockedInt != 0 {
43 print("invalid m->lockedInt = ", _g_.m.lockedInt, "\n")
44 throw("internal lockOSThread error")
45 }
46 // 将g加入p的空闲队列
47 gfput(_g_.m.p.ptr(), gp)
48 if locked {
49 // The goroutine may have locked this thread because
50 // it put it in an unusual kernel state. Kill it
51 // rather than returning it to the thread pool.
52
53 // Return to mstart, which will release the P and exit
54 // the thread.
55 if GOOS != "plan9" { // See golang.org/issue/22227.
56 gogo(&_g_.m.g0.sched)
57 } else {
58 // Clear lockedExt on plan9 since we may end up re-using
59 // this thread.
60 _g_.m.lockedExt = 0
61 }
62 }
63 // 执行下一轮调度
64 schedule()
65 }
3.2 P源码部分
3.2.1 P的结构
1 // runtime/runtime2.go
2
3 type p struct {
4 // 全局变量allp中的索引位置
5 id int32
6 // p的状态标识
7 status uint32 // one of pidle/prunning/...
8 link puintptr
9 // 调用schedule的次数,每次调用schedule这个值会加1
10 schedtick uint32 // incremented on every scheduler call
11 // 系统调用的次数,每次进行系统调用加1
12 syscalltick uint32 // incremented on every system call
13 // 用于sysmon协程记录被监控的p的系统调用时间和运行时间
14 sysmontick sysmontick // last tick observed by sysmon
15 // 指向绑定的m,p如果是idle状态这个值为nil
16 m muintptr // back-link to associated m (nil if idle)
17 // 用于分配微小对象和小对象的一个块的缓存空间,里面有各种不同等级的span
18 mcache *mcache
19 // 一个chunk大小(512kb)的内存空间,用来对堆上内存分配的缓存优化达到无锁访问的目的
20 pcache pageCache
21 raceprocctx uintptr
22
23 deferpool [5][]*_defer // pool of available defer structs of different sizes (see panic.go)
24 deferpoolbuf [5][32]*_defer
25
26 // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
27 // 可以分配给g的id的缓存,每次会一次性申请16个
28 goidcache uint64
29 goidcacheend uint64
30
31 // Queue of runnable goroutines. Accessed without lock.
32 // 本地可运行的G队列的头部和尾部,达到无锁访问
33 runqhead uint32
34 runqtail uint32
35 // 本地可运行的g队列,是一个使用数组实现的循环队列
36 runq [256]guintptr
37 // 下一个待运行的g,这个g的优先级最高
38 // 如果当前g运行完后还有剩余可用时间,那么就应该运行这个runnext的g
39 runnext guintptr
40
41 // Available G's (status == Gdead)
42 // p上的空闲队列列表
43 gFree struct {
44 gList
45 n int32
46 }
47
48 ............
49 // 用于内存对齐
50 _ uint32 // Alignment for atomic fields below
51 .......................
52 // 是否被抢占
53 preempt bool
54
55 // Padding is no longer needed. False sharing is now not a worry because p is large enough
56 // that its size class is an integer multiple of the cache line size (for any of our architectures).
57 }
3.2.2 P的状态

3.2.3 P的创建
1 ...........................
2 CALL runtime·args(SB)
3 CALL runtime·osinit(SB)
4 CALL runtime·schedinit(SB)
5 ...........................
1 func schedinit() {
2 .................
3 lock(&sched.lock)
4 sched.lastpoll = uint64(nanotime())
5 procs := ncpu
6 if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
7 procs = n
8 }
9 if procresize(procs) != nil {
10 throw("unknown runnable goroutine during bootstrap")
11 }
12 unlock(&sched.lock)
13
14 // World is effectively started now, as P's can run.
15 worldStarted()
16 .....................
17 }
1 func procresize(nprocs int32) *p {
2 .............................
3 old := gomaxprocs
4 ......................
5 if nprocs > int32(len(allp)) {
6 // Synchronize with retake, which could be running
7 // concurrently since it doesn't run on a P.
8 lock(&allpLock)
9 if nprocs <= int32(cap(allp)) {
10 // 如果需要的p小于allp这个全局变量(切片)的cap能力,取其中的一部分
11 allp = allp[:nprocs]
12 } else {
13 // 否则创建nprocs数量的p,并把allp的中复制给nallp
14 nallp := make([]*p, nprocs)
15 // Copy everything up to allp's cap so we
16 // never lose old allocated Ps.
17 copy(nallp, allp[:cap(allp)])
18 allp = nallp
19 }
20 ....................................
21 unlock(&allpLock)
22 }
23
24 // 进行p的初始化
25 for i := old; i < nprocs; i++ {
26 pp := allp[i]
27 if pp == nil {
28 pp = new(p)
29 }
30 pp.init(i)
31 atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
32 }
33 ...............................
34 return runnablePs
35 }
3.3 M源码部分
3.3.1 M的结构
1 type m struct {
2 // 每个m都有一个对应的g0线程,用来执行调度代码,
3 // 当需要执行用户代码的时候,g0会与用户goroutine发生协程栈切换
4 g0 *g // goroutine with scheduling stack
5 morebuf gobuf // gobuf arg to morestack
6 ........................
7 // tls作为线程的本地存储
8 // 其中可以在任意时刻获取绑定到当前线程上的协程g、结构体m、逻辑处理器p、特殊协程g0等信息
9 tls [tlsSlots]uintptr // thread-local storage (for x86 extern register)
10 mstartfn func()
11 // 指向正在运行的goroutine对象
12 curg *g // current running goroutine
13 caughtsig guintptr // goroutine running during fatal signal
14 // 与当前工作线程绑定的p
15 p puintptr // attached p for executing go code (nil if not executing go code)
16 nextp puintptr
17 oldp puintptr // the p that was attached before executing a syscall
18 id int64
19 mallocing int32
20 throwing int32
21 // 与禁止抢占相关的字段,如果该字段不等于空字符串,要保持curg一直在这个m上运行
22 preemptoff string // if != "", keep curg running on this m
23 // locks也是判断g能否被抢占的一个标识
24 locks int32
25 dying int32
26 profilehz int32
27 // spining为true标识当前m正在处于自己找工作的自旋状态,
28 // 首先检查全局队列看是否有工作,然后检查network poller,尝试执行GC任务
29 //或者偷一部分工作,如果都没有则会进入休眠状态
30 spinning bool // m is out of work and is actively looking for work
31 // 表示m正阻塞在note上
32 blocked bool // m is blocked on a note
33 .........................
34 doesPark bool // non-P running threads: sysmon and newmHandoff never use .park
35 // 没有goroutine需要运行时,工作线程睡眠在这个park成员上
36 park note
37 // 记录所有工作线程的一个链表
38 alllink *m // on allm
39 schedlink muintptr
40 lockedg guintptr
41 createstack [32]uintptr // stack that created this thread.
42 .............................
43 }
3.3.2 M的状态
1 mstart
2 |
3 v
4 +------+ 找不到可执行任务 +-------+
5 |unspin| ----------------------------> |spining|
6 | | <---------------------------- | |
7 +------+ 找到可执行任务 +-------+
8 ^ | stopm
9 | wakep v
10 notewakeup <------------------------- notesleep
3.3.3 M的创建
1 // newm创建一个新的m,将从fn或者调度程序开始执行
2 func newm(fn func(), _p_ *p, id int64) {
3 // 这里实现m的创建
4 mp := allocm(_p_, fn, id)
5 mp.doesPark = (_p_ != nil)
6 mp.nextp.set(_p_)
7 mp.sigmask = initSigmask
8 if gp := getg(); gp != nil && gp.m != nil && (gp.m.lockedExt != 0 || gp.m.incgo) && GOOS != "plan9" {
9 // 我们处于锁定的M或可能由C启动的线程。此线程的内核状态可能很奇怪(用户可能已将其锁定为此目的)。
10 // 我们不想将其克隆到另一个线程中。相反,请求一个已知良好的线程为我们创建线程。
11 lock(&newmHandoff.lock)
12 if newmHandoff.haveTemplateThread == 0 {
13 throw("on a locked thread with no template thread")
14 }
15 mp.schedlink = newmHandoff.newm
16 newmHandoff.newm.set(mp)
17 if newmHandoff.waiting {
18 newmHandoff.waiting = false
19 notewakeup(&newmHandoff.wake)
20 }
21 unlock(&newmHandoff.lock)
22 return
23 }
24 // 这里分配真正的的操作系统线程
25 newm1(mp)
26 }
1 func allocm(_p_ *p, fn func(), id int64) *m {
2 // 获取当前运行的g
3 _g_ := getg()
4 // 将_g_对应的m的locks加1,防止被抢占
5 acquirem() // disable GC because it can be called from sysmon
6 if _g_.m.p == 0 {
7 acquirep(_p_) // temporarily borrow p for mallocs in this function
8 }
9
10 // 检查是有有空闲的m可以释放,主要目的是释放m上的g0占用的系统栈
11 if sched.freem != nil {
12 lock(&sched.lock)
13 var newList *m
14 for freem := sched.freem; freem != nil; {
15 if freem.freeWait != 0 {
16 next := freem.freelink
17 freem.freelink = newList
18 newList = freem
19 freem = next
20 continue
21 }
22 // stackfree must be on the system stack, but allocm is
23 // reachable off the system stack transitively from
24 // startm.
25 systemstack(func() {
26 stackfree(freem.g0.stack)
27 })
28 freem = freem.freelink
29 }
30 sched.freem = newList
31 unlock(&sched.lock)
32 }
33 // 创建一个m结构体
34 mp := new(m)
35 mp.mstartfn = fn // 将fn设置为m启动后执行的函数
36 mcommoninit(mp, id)
37
38 // In case of cgo or Solaris or illumos or Darwin, pthread_create will make us a stack.
39 // Windows and Plan 9 will layout sched stack on OS stack.
40 if iscgo || mStackIsSystemAllocated() {
41 mp.g0 = malg(-1)
42 } else {
43 // 设置m对应的g0,并设置对应大小的g0协程栈,g0是8kb
44 mp.g0 = malg(8192 * sys.StackGuardMultiplier)
45 }
46 // 设置g0对应的m
47 mp.g0.m = mp
48
49 if _p_ == _g_.m.p.ptr() {
50 releasep()
51 }
52 // 解除_g_的m的禁止抢占状态。
53 releasem(_g_.m)
54
55 return mp
56 }
1 func canPreemptM(mp *m) bool {
2 return mp.locks == 0 && mp.mallocing == 0 && mp.preemptoff == "" && mp.p.ptr().status == _Prunning
3 }
1 func newm1(mp *m) {
2 ..............
3 execLock.rlock() // Prevent process clone.
4 // 创建一个系统线程,并且传入该 mp 绑定的 g0 的栈顶指针
5 // 让系统线程执行 mstart 函数,后面的逻辑都在 mstart 函数中
6 newosproc(mp)
7 execLock.runlock()
8 }

1 func newosproc(mp *m) {
2 stk := unsafe.Pointer(mp.g0.stack.hi)
3 .........................
4 var oset sigset
5 sigprocmask(_SIG_SETMASK, &sigset_all, &oset)
6 ret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))
7 sigprocmask(_SIG_SETMASK, &oset, nil)
8 .........................
9 }
3.3.4 M的休眠
1 func stopm() {
2 _g_ := getg()
3 .....................
4 lock(&sched.lock)
5 // 首先将m放到全局空闲链表中,这里要加锁访问全局链表
6 mput(_g_.m)
7 unlock(&sched.lock)
8 // 进入睡眠状态
9 mPark()
10 // 将m与p解绑
11 acquirep(_g_.m.nextp.ptr())
12 _g_.m.nextp = 0
13 }
14
15 func mPark() {
16 g := getg()
17 for {
18 // 使工作线程休眠在park字段上
19 notesleep(&g.m.park)
20 noteclear(&g.m.park)
21 if !mDoFixup() {
22 return
23 }
24 }
25 }
1 //uaddr指向一个地址,val代表这个地址期待的值,当*uaddr==val时,才会进行wait
2 int futex_wait(int *uaddr, int val);
3 //唤醒n个在uaddr指向的锁变量上挂起等待的进程
4 int futex_wake(int *uaddr, int n);
3.3.5 M的唤醒
1 // Tries to add one more P to execute G's.
2 // Called when a G is made runnable (newproc, ready).
3 func wakep() {
4 if atomic.Load(&sched.npidle) == 0 {
5 return
6 }
7 // be conservative about spinning threads
8 // 如果有其他的M处于自旋状态,那么就不管了,直接返回,因为自旋的M会拼命找G来运行的
9 if atomic.Load(&sched.nmspinning) != 0 || !atomic.Cas(&sched.nmspinning, 0, 1) {
10 return
11 }
12 startm(nil, true)
13 }
1 func startm(_p_ *p, spinning bool) {
2 // 禁止抢占,防止GC垃圾回收
3 mp := acquirem()
4 lock(&sched.lock)
5 // 如果P为nil,则尝试获取一个空闲P
6 if _p_ == nil {
7 _p_ = pidleget()
8 if _p_ == nil { // 没有空闲的P,则解除禁止抢占,直接返回
9 unlock(&sched.lock)
10 if spinning {
11 if int32(atomic.Xadd(&sched.nmspinning, -1)) < 0 {
12 throw("startm: negative nmspinning")
13 }
14 }
15 releasem(mp)
16 return
17 }
18 }
19 // 获取一个空闲的M
20 nmp := mget()
21 if nmp == nil {
22 // 如果没有空闲的m则新建一个
23 id := mReserveID()
24 unlock(&sched.lock)
25
26 var fn func()
27 if spinning {
28 // The caller incremented nmspinning, so set m.spinning in the new M.
29 fn = mspinning
30 }
31 newm(fn, _p_, id)
32 // Ownership transfer of _p_ committed by start in newm.
33 // Preemption is now safe.
34 releasem(mp)
35 return
36 }
37 unlock(&sched.lock)
38 ......................
39 //标记该M是否在自旋
40 nmp.spinning = spinning
41 // 暂存P
42 nmp.nextp.set(_p_)
43 // 唤醒M
44 notewakeup(&nmp.park)
45 // Ownership transfer of _p_ committed by wakeup. Preemption is now
46 // safe.
47 releasem(mp)
48 }
1 //唤醒n个在uaddr指向的锁变量上挂起等待的进程
2 int futex_wake(int *uaddr, int n);
4 启动过程
4.1 Go scheduler结构
1 type schedt struct {
2 // 用来为goroutine生成唯一id,需要以原子访问形式进行访问
3 // 放在struct顶部,以便在32位系统上可以进行对齐
4 goidgen uint64
5 ...................
6 lock mutex
7 // 空闲的m组成的链表
8 midle muintptr // idle m's waiting for work
9 // 空闲的工作线程数量
10 nmidle int32 // number of idle m's waiting for work
11 // 空闲的且被lock的m的数量
12 nmidlelocked int32 // number of locked m's waiting for work
13 mnext int64 // number of m's that have been created and next M ID
14 // 表示最多能够创建的工作线程数量
15 maxmcount int32 // maximum number of m's allowed (or die)
16 nmsys int32 // number of system m's not counted for deadlock
17 nmfreed int64 // cumulative number of freed m's
18 // 整个goroutine的数量,能够自动保持更新
19 ngsys uint32 // number of system goroutines; updated atomically
20 // 由空闲的p结构体对象组成的链表
21 pidle puintptr // idle p's
22 // 空闲的p结构体对象的数量
23 npidle uint32
24 // 自旋的m的数量
25 nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.
26
27 // Global runnable queue.
28 // 全局的的g的队列
29 runq gQueue
30 runqsize int32
31
32 disable struct {
33 // user disables scheduling of user goroutines.
34 user bool
35 runnable gQueue // pending runnable Gs
36 n int32 // length of runnable
37 }
38
39 // Global cache of dead G's.
40 // 空闲的g队列,这里面g的状态为_Gdead
41 gFree struct {
42 lock mutex
43 stack gList // Gs with stacks
44 noStack gList // Gs without stacks
45 n int32
46 }
47 .................
48 // 空闲的m队列
49 freem *m
50 .....................
51 // 上次修改gomaxprocs的时间
52 procresizetime int64 // nanotime() of last change to gomaxprocs
53 ......................
54 }
1 // runtime/runtime2.go
2 var (
3 // 保存所有的m
4 allm *m
5 // p的最大个数,默认等于cpu核数
6 gomaxprocs int32
7 // cpu的核数,程序启动时会调用osinit获取ncpu值
8 ncpu int32
9 // 调度器结构体对象,记录了调度器的工作状态
10 sched schedt
11 newprocs int32
12
13 allpLock mutex
14 // 全局的p队列
15 allp []*p
16 )
17
18 // runtime/proc.go
19 var (
20 // 代表进程主线程的m0对象
21 m0 m
22 // m0的g0
23 g0 g
24 // 全局的mcache对象,管理各种类型的span队列
25 mcache0 *mcache
26 raceprocctx0 uintptr
27 )
4.2 启动流程

1 // runtime·rt0_go
2
3 // 程序刚启动的时候必定有一个线程启动(主线程)
4 // 将当前的栈和资源保存在g0
5 // 将该线程保存在m0
6 // tls: Thread Local Storage
7 // set the per-goroutine and per-mach "registers"
8 get_tls(BX)
9 LEAQ runtime·g0(SB), CX
10 MOVQ CX, g(BX)
11 LEAQ runtime·m0(SB), AX
12
13 // m0和g0互相绑定
14 // save m->g0 = g0
15 MOVQ CX, m_g0(AX)
16 // save m0 to g0->m
17 MOVQ AX, g_m(CX)
18 // 处理args
19 CALL runtime·args(SB)
20 // os初始化, os_linux.go
21 CALL runtime·osinit(SB)
22 // 调度系统初始化, proc.go
23 CALL runtime·schedinit(SB)
24
25 // 创建一个goroutine,然后开启执行程序
26 // create a new goroutine to start program
27 MOVQ $runtime·mainPC(SB), AX // entry
28 PUSHQ AX
29 PUSHQ $0 // arg size
30 CALL runtime·newproc(SB)
31 POPQ AX
32 POPQ AX
33
34 // start this M
35 // 启动线程,并且启动调度系统
36 CALL runtime·mstart(SB)


4.3 调度器初始化
1 func schedinit() {
2 ................
3 // g0
4 _g_ := getg()
5 if raceenabled {
6 _g_.racectx, raceprocctx0 = raceinit()
7 }
8 // 最多启动10000个工作线程
9 sched.maxmcount = 10000
10
11 // The world starts stopped.
12 worldStopped()
13
14 moduledataverify()
15 // 初始化协程堆栈,包括专门分配小栈的stackpool和分配大栈的stackLarge
16 stackinit()
17 // 整个堆内存的初始化分配
18 mallocinit()
19 fastrandinit() // must run before mcommoninit
20 // 初始化m0
21 mcommoninit(_g_.m, -1)
22 cpuinit() // must run before alginit
23 alginit() // maps must not be used before this call
24 modulesinit() // provides activeModules
25 typelinksinit() // uses maps, activeModules
26 itabsinit() // uses activeModules
27
28 sigsave(&_g_.m.sigmask)
29 initSigmask = _g_.m.sigmask
30
31 if offset := unsafe.Offsetof(sched.timeToRun); offset%8 != 0 {
32 println(offset)
33 throw("sched.timeToRun not aligned to 8 bytes")
34 }
35
36 goargs()
37 goenvs()
38 parsedebugvars()
39 gcinit()
40 // 这部分是初始化p,
41 // cpu有多少个核数就初始化多少个p
42 lock(&sched.lock)
43 sched.lastpoll = uint64(nanotime())
44 procs := ncpu
45 if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
46 procs = n
47 }
48 if procresize(procs) != nil {
49 throw("unknown runnable goroutine during bootstrap")
50 }
51 unlock(&sched.lock)
52
53 // World is effectively started now, as P's can run.
54 worldStarted()
55 }
4.4 启动调度系统
1 func mstart0() {
2 // 这里获取的是g0在系统栈上执行
3 _g_ := getg()
4 .............
5 mstart1()
6 .............
7 }
8
9 func mstart1(dummy int32) {
10 _g_ := getg()
11 // 确保g是系统栈上的g0
12 // 调度器只在g0上执行
13 if _g_ != _g_.m.g0 {
14 throw("bad runtime·mstart")
15 }
16 ...
17 // 初始化m,主要是设置线程的备用信号堆栈和信号掩码
18 minit()
19 // 如果当前g的m是初始m0,执行mstartm0()
20 if _g_.m == &m0 {
21 // 对于初始m,需要一些特殊处理,主要是设置系统信号量的处理函数
22 mstartm0()
23 }
24 // 如果有m的起始任务函数,则执行,比如 sysmon 函数
25 // 对于m0来说,是没有 mstartfn 的
26 if fn := _g_.m.mstartfn; fn != nil {
27 fn()
28 }
29 if _g_.m.helpgc != 0 {
30 _g_.m.helpgc = 0
31 stopm()
32 } else if _g_.m != &m0 { // 如果不是m0,需要绑定p
33 // 绑定p
34 acquirep(_g_.m.nextp.ptr())
35 _g_.m.nextp = 0
36 }
37 // 进入调度循环,永不返回
38 schedule()
39 }
4.5 runtime.main函数
1 // The main goroutine.
2 func main() {
3 // 获取 main goroutine
4 g := getg()
5 ...
6 // 在系统栈上运行 sysmon
7 systemstack(func() {
8 // 分配一个新的m,运行sysmon系统后台监控
9 // (定期垃圾回收和调度抢占)
10 newm(sysmon, nil)
11 })
12 ...
13 // 确保是主线程
14 if g.m != &m0 {
15 throw("runtime.main not on m0")
16 }
17 // runtime 内部 init 函数的执行,编译器动态生成的。
18 runtime_init() // must be before defer
19 ...
20 // gc 启动一个goroutine进行gc清扫
21 gcenable()
22 ...
23 // 执行init函数,编译器动态生成的,
24 // 包括用户定义的所有的init函数。
25 // make an indirect call,
26 // as the linker doesn't know the address of
27 // the main package when laying down the runtime
28 fn := main_init
29 fn()
30 ...
31 // 真正的执行main func in package main
32 // make an indirect call,
33 // as the linker doesn't know the address of
34 // the main package when laying down the runtime
35 fn = main_main
36 fn()
37 ...
38 // 退出程序
39 exit(0)
40 // 为何这里还需要for循环?
41 // 下面的for循环一定会导致程序崩掉,这样就确保了程序一定会退出
42 for {
43 var x *int32
44 *x = 0
45 }
46 }
5 协程的调度策略
5.1 调度策略
1 func schedule() {
2 _g_ := getg()
3 ...
4 top:
5 // 如果当前GC需要停止整个世界(STW), 则调用gcstopm休眠当前的M
6 if sched.gcwaiting != 0 {
7 // 为了STW,停止当前的M
8 gcstopm()
9 // STW结束后回到 top
10 goto top
11 }
12 ...
13 var gp *g
14 var inheritTime bool
15 ...
16 if gp == nil {
17 // 为了防止全局队列中的g永远得不到运行,所以go语言让p每执行61调度,
18 // 就需要优先从全局队列中获取一个G到当前P中,并执行下一个要执行的G
19 if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
20 lock(&sched.lock)
21 gp = globrunqget(_g_.m.p.ptr(), 1)
22 unlock(&sched.lock)
23 }
24 }
25 if gp == nil {
26 // 从p的本地队列中获取
27 gp, inheritTime = runqget(_g_.m.p.ptr())
28 if gp != nil && _g_.m.spinning {
29 throw("schedule: spinning with local work")
30 }
31 }
32 if gp == nil {
33 // 想尽办法找到可运行的G,找不到就不用返回了
34 gp, inheritTime = findrunnable() // blocks until work is available
35 }
36 ...
37 // println("execute goroutine", gp.goid)
38 // 找到了g,那就执行g上的任务函数
39 execute(gp, inheritTime)
40 }
1 func findrunnable() (gp *g, inheritTime bool) {
2 _g_ := getg()
3
4 top:
5 ............................
6 // 本地队列中检查
7 if gp, inheritTime := runqget(_p_); gp != nil {
8 return gp, inheritTime
9 }
10 // 从全局队列中寻找
11 if sched.runqsize != 0 {
12 lock(&sched.lock)
13 gp := globrunqget(_p_, 0)
14 unlock(&sched.lock)
15 if gp != nil {
16 return gp, false
17 }
18 }
19 // 从就绪的网络协程中查找
20 if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {
21 if list := netpoll(0); !list.empty() { // non-blocking
22 gp := list.pop()
23 injectglist(&list)
24 casgstatus(gp, _Gwaiting, _Grunnable)
25 if trace.enabled {
26 traceGoUnpark(gp, 0)
27 }
28 return gp, false
29 }
30 }
31
32 // 进入自旋状态
33 procs := uint32(gomaxprocs)
34 if _g_.m.spinning || 2*atomic.Load(&sched.nmspinning) < procs-atomic.Load(&sched.npidle) {
35 if !_g_.m.spinning {
36 _g_.m.spinning = true
37 atomic.Xadd(&sched.nmspinning, 1)
38 }
39 // 从其他p的本地队列中偷工作
40 gp, inheritTime, tnow, w, newWork := stealWork(now)
41 ..............................
42 }
43 }

5.2 调度时机
5.2.1 主动调度
1 func Gosched() {
2 checkTimeouts()
3 mcall(gosched_m)
4 }
5
6 // Gosched continuation on g0.
7 func gosched_m(gp *g) {
8 if trace.enabled {
9 traceGoSched()
10 }
11 goschedImpl(gp)
12 }
13
14 func goschedImpl(gp *g) {
15 status := readgstatus(gp)
16 if status&^_Gscan != _Grunning {
17 dumpgstatus(gp)
18 throw("bad g status")
19 }
20 // 更改g的运行状态
21 casgstatus(gp, _Grunning, _Grunnable)
22 // 接触g和m的绑定关系
23 dropg()
24 // 将g放入全局队列中
25 lock(&sched.lock)
26 globrunqput(gp)
27 unlock(&sched.lock)
28
29 schedule()
30 }
5.2.2 被动调度
1 func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
2 if reason != waitReasonSleep {
3 checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
4 }
5 // 禁止抢占和GC
6 mp := acquirem()
7 gp := mp.curg // 过去当前m上运行的g
8 status := readgstatus(gp)
9 if status != _Grunning && status != _Gscanrunning {
10 throw("gopark: bad g status")
11 }
12 // 设置相关的wait字段
13 mp.waitlock = lock
14 mp.waitunlockf = unlockf
15 gp.waitreason = reason
16 mp.waittraceev = traceEv
17 mp.waittraceskip = traceskip
18 releasem(mp)
19 // can't do anything that might move the G between Ms here.
20 mcall(park_m)
21 }
22
23 // park continuation on g0.
24 func park_m(gp *g) {
25 _g_ := getg()
26 // 变更g的状态
27 casgstatus(gp, _Grunning, _Gwaiting)
28 // 接触g与m的绑定关系
29 dropg()
30 // 根据被动调度不同原因,执行不同的waitunlockf函数
31 if fn := _g_.m.waitunlockf; fn != nil {
32 ok := fn(gp, _g_.m.waitlock)
33 _g_.m.waitunlockf = nil
34 _g_.m.waitlock = nil
35 if !ok {
36 if trace.enabled {
37 traceGoUnpark(gp, 2)
38 }
39 casgstatus(gp, _Gwaiting, _Grunnable)
40 execute(gp, true) // Schedule it back, never returns.
41 }
42 }
43 // 进入下一轮调度
44 schedule()
45 }
1 func ready(gp *g, traceskip int, next bool) {
2 ..............
3 status := readgstatus(gp)
4
5 // Mark runnable.
6 _g_ := getg()
7 mp := acquirem() // disable preemption because it can be holding p in a local var
8 ...............
9 // 变更状态之后,放入p的局部运行队列中
10 casgstatus(gp, _Gwaiting, _Grunnable)
11 runqput(_g_.m.p.ptr(), gp, next)
12 wakep()
13 releasem(mp)
14 }
5.2.3 抢占式调度
1 func retake(now int64) uint32 {
2 n := 0
3 lock(&allpLock)
4 // 遍历所有的P
5 for i := 0; i < len(allp); i++ {
6 _p_ := allp[i]
7 if _p_ == nil {
8 // This can happen if procresize has grown
9 // allp but not yet created new Ps.
10 continue
11 }
12 pd := &_p_.sysmontick
13 s := _p_.status
14 sysretake := false
15
16 if s == _Prunning || s == _Psyscall {
17 // 判断如果g的运行时间过长则抢占
18 t := int64(_p_.schedtick)
19 if int64(pd.schedtick) != t {
20 pd.schedtick = uint32(t)
21 pd.schedwhen = now
22 } else if pd.schedwhen+forcePreemptNS <= now {
23 // 如果连续运行10ms则进行抢占
24 preemptone(_p_)
25 sysretake = true
26 }
27 }
28 // 针对系统调用情况进行抢占
29 // 如果p的运行队列中有等待运行的g则抢占
30 // 如果没有空闲的p则进行抢占
31 // 系统调用时间超过10ms则进行抢占
32 if s == _Psyscall {
33 // Retake P from syscall if it's there for more than 1 sysmon tick (at least 20us).
34 t := int64(_p_.syscalltick)
35 if !sysretake && int64(pd.syscalltick) != t {
36 pd.syscalltick = uint32(t)
37 pd.syscallwhen = now
38 continue
39 }
40 // On the one hand we don't want to retake Ps if there is no other work to do,
41 // but on the other hand we want to retake them eventually
42 // because they can prevent the sysmon thread from deep sleep.
43 if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {
44 continue
45 }
46 // Drop allpLock so we can take sched.lock.
47 unlock(&allpLock)
48 // Need to decrement number of idle locked M's
49 // (pretending that one more is running) before the CAS.
50 // Otherwise the M from which we retake can exit the syscall,
51 // increment nmidle and report deadlock.
52 incidlelocked(-1)
53 if atomic.Cas(&_p_.status, s, _Pidle) {
54 if trace.enabled {
55 traceGoSysBlock(_p_)
56 traceProcStop(_p_)
57 }
58 n++
59 _p_.syscalltick++
60 handoffp(_p_)
61 }
62 incidlelocked(1)
63 lock(&allpLock)
64 }
65 }
66 unlock(&allpLock)
67 return uint32(n)
68 }
6 参考资料
- Go语言的调度模型:https://www.cnblogs.com/lvpengbo/p/13973906.html
- 深度揭秘Go语言 sync.Pool:https://www.cnblogs.com/qcrao-2018/p/12736031.html
- https://toutiao.io/posts/2gic8x/preview
- 万字长文深入浅出go runtime:https://zhuanlan.zhihu.com/p/95056679
- 深入go runtime的调度:https://zboya.github.io/post/go_scheduler/
- 字节Go面试:https://leetcode-cn.com/circle/discuss/A0YstA/
- golang调度学习-调度过程:https://blog.csdn.net/diaosssss/article/details/93066804
- Go调度器中的三种结构:G、P、M:https://blog.csdn.net/chenxun_2010/article/details/103696394
- Go语言的调度模型:https://www.cnblogs.com/lvpengbo/p/13973906.html
- Go GMP的调度模型:https://zhuanlan.zhihu.com/p/413970952
- 详细剖析Go语言调度模型的设计:https://www.elecfans.com/d/1670400.html
- Go的核心goroutine sysmon:https://blog.csdn.net/RA681t58CJxsgCkJ31/article/details/108633220
- 一文教你搞懂Go中栈操作:https://zhuanlan.zhihu.com/p/364813527
- 详解Go语言调度循环代码实现:https://www.luozhiyun.com/archives/448
- goroutine的创建、休眠与恢复:https://zhuanlan.zhihu.com/p/386595432
GO GMP协程调度实现原理 5w字长文史上最全的更多相关文章
- 图解Go协程调度原理,小白都能理解
阅读本文仅需五分钟,golang协程调度原理,小白也能看懂,超实用. 什么是协程 对于进程.线程,都是有内核进行调度,有CPU时间片的概念,进行抢占式调度.协程,又称微线程,纤程.英文名Corouti ...
- Golang源码探索(二) 协程的实现原理
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻 ...
- Golang源码探索(二) 协程的实现原理(转)
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱,虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底 ...
- 协程概念,原理及实现(c++和node.js实现)
协程 什么是协程 wikipedia 的定义: 协程是一个无优先级的子程序调度组件,允许子程序在特点的地方挂起恢复. 线程包含于进程,协程包含于线程.只要内存足够,一个线程中可以有任意多个协程,但某一 ...
- Openresty Lua协程调度机制
写在前面 OpenResty(后面简称:OR)是一个基于Nginx和Lua的高性能Web平台,它内部集成大量的Lua API以及第三方模块,可以利用它快速搭建支持高并发.极具动态性和扩展性的Web应用 ...
- go协程调度
目录 前言 1. 线程池的缺陷 2.Goroutine 调度器 3.调度策略 3.1 队列轮转 3.2 系统调用 3.3 工作量窃取 4.GOMAXPROCS设置对性能的影响 参考 前言 Gorout ...
- Kotlin协程解析系列(上):协程调度与挂起
vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...
- Golang 协程调度
一.线程模型 N:1模型,N个用户空间线程在1个内核空间线程上运行.优势是上下文切换非常快但是无法利用多核系统的优点. 1:1模型,1个内核空间线程运行一个用户空间线程.这种充分利用了多核系统的优势但 ...
- 深入浅出!从语义角度分析隐藏在Unity协程背后的原理
Unity的协程使用起来比较方便,但是由于其封装和隐藏了太多细节,使其看起来比较神秘.比如协程是否是真正的异步执行?协程与线程到底是什么关系?本文将从语义角度来分析隐藏在协程背后的原理,并使用C++来 ...
随机推荐
- 自定义View的onDraw 函数不执行
解决办法: 在自定义的View 的构造方法中添加一句话: this.setWillNotDraw(false);解释:那么加这条语句的作用是什么?先看API: If this ...
- 访问控制中默认,public,private,protected区别?
2.继承的访问控制: (比如一个类中的protected成员对于"不同的包中的非子类"是不可见的. 说明:1.任何public的内容可以被从任何地方访问. 2.private的成员 ...
- 在react项目中使用redux-thunk,react-redux,redux;使用总结
先看是什么,再看怎么用: redux-thunk是一个redux的中间件,用来处理redux中的复杂逻辑,比如异步请求: redux-thunk中间件可以让action创建函数先不返回一个action ...
- [ SOS ] 版本控制工具 笔记
https://www.cnblogs.com/yeungchie/ soscmd 创建工作区 soscmd newworkarea $serverName $projectName [$path] ...
- 导出带标签的tar包(docker)-解决导出不带标签的麻烦
需求:在docker的本地镜像库中导出tar包给其他节点使用. 如果使用:docker save -o package.tar e82656a6fc 这样形式导出的tar包,安装之后标签会消失解决办法 ...
- 在容器使用stress指令进行负载压测
安装stressstress是一个linux下的压力测试工具,专门为那些想要测试自己的系统,完全高负荷和监督这些设备运行的用户 在容器中安装docker容器压测工具 stress #先安装一些基础工具 ...
- 论文解读(MLGCL)《Multi-Level Graph Contrastive Learning》
论文信息 论文标题:Structural and Semantic Contrastive Learning for Self-supervised Node Representation Learn ...
- Python Pandas库 初步使用
用pandas+numpy读取UCI iris数据集中鸢尾花的萼片.花瓣长度数据,进行数据清理,去重,排序,并求出和.累积和.均值.标准差.方差.最大值.最小值
- php进制转换
前端html页面代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset=&qu ...
- 2021.11.11 P4052 [JSOI2007]文本生成器(AC自动机+DP)
2021.11.11 P4052 [JSOI2007]文本生成器(AC自动机+DP) https://www.luogu.com.cn/problem/P4052 题意: JSOI 交给队员 ZYX ...