1. 创建一个goroutine
通过go创建一个goroutine。底层通过runtime.newproc()创建。
创建一个goroutine包括两部分:
- 创建一个g对象
优先从g空闲列表获取一个,没有则新建。将该g放入待执行队列(本地,或者全局)。 - 执行该g对象
新创建的goroutine并不会立即执行。要执行需要先获取一个空闲的P,没有则等待有P空闲出来时由调度器执行。有空闲P时,还需要一个m取实际执行,如果有空闲m,则唤醒该m,如果没有,则新建一个m线程。
由于是优先空闲的P,因此刚开始会对所有的P依次进行分配。
go my_goroutine();
//对应汇编
0x0034 00052 (mygoroutine.go:13) CALL runtime.newproc(SB)
2. runtime.newproc()
切换到系统栈(g0,或者gsinal),执行newproc1创建一个p。
- 获取调用者地址,用来返回。
- 通过systemstack调用newproc1
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg()
pc := getcallerpc()
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, gp, pc)
})
}
2.1 systemstack
切换到系统栈,然后调用传入函数。
- g0是线程绑定的。
- 如果是g0,或者gsignal goroutine,则直接调用调用传入的函数。
- 否则,先切换到g0栈,再执行传入函数。
- 执行完毕,切换回调用者栈。
// systemstack runs fn on a system stack.
// If systemstack is called from the per-OS-thread (g0) stack, or
// if systemstack is called from the signal handling (gsignal) stack,
// systemstack calls fn directly and returns.
// Otherwise, systemstack is being called from the limited stack
// of an ordinary goroutine. In this case, systemstack switches
// to the per-OS-thread stack, calls fn, and switches back.
// It is common to use a func literal as the argument, in order
// to share inputs and outputs with the code around the call
// to system stack:
//
// ... set up y ...
// systemstack(func() {
// x = bigcall(y)
// })
// ... use x ...
//
//go:noescape
func systemstack(fn func())
2.2 systemstack 汇编代码
// func systemstack(fn func())
TEXT runtime·systemstack(SB), NOSPLIT, $0-8
MOVD fn+0(FP), R3 // R3 = fn
MOVD R3, R26 // context
MOVD g_m(g), R4 // R4 = m
// 判断当前的g是否gsignal
MOVD m_gsignal(R4), R5 // R5 = gsignal
CMP g, R5
BEQ noswitch //在当前栈直接执行
// 判断当前的g是否g0
MOVD m_g0(R4), R5 // R5 = g0
CMP g, R5
BEQ noswitch //在当前栈直接执行
// 不是gsignal,也不是g0,栈切换到g0
MOVD m_curg(R4), R6
CMP g, R6
BEQ switch
// 异常处理,进程退出
// Bad: g is not gsignal, not g0, not curg. What is it?
// Hide call from linker nosplit analysis.
MOVD $runtime·badsystemstack(SB), R3
BL (R3)
B runtime·abort(SB)
switch:
// save our state in g->sched. Pretend to
// be systemstack_switch if the G stack is scanned.
MOVD $runtime·systemstack_switch(SB), R6
ADD $8, R6 // get past prologue
MOVD R6, (g_sched+gobuf_pc)(g)
MOVD RSP, R0
MOVD R0, (g_sched+gobuf_sp)(g)
MOVD R29, (g_sched+gobuf_bp)(g)
MOVD $0, (g_sched+gobuf_lr)(g)
MOVD g, (g_sched+gobuf_g)(g)
// switch to g0
MOVD R5, g
BL runtime·save_g(SB)
MOVD (g_sched+gobuf_sp)(g), R3
// make it look like mstart called systemstack on g0, to stop traceback
SUB $16, R3
AND $~15, R3
MOVD $runtime·mstart(SB), R4
MOVD R4, 0(R3)
MOVD R3, RSP
MOVD (g_sched+gobuf_bp)(g), R29
// call target function
MOVD 0(R26), R3 // code pointer
BL (R3)
// switch back to g
MOVD g_m(g), R3
MOVD m_curg(R3), g
BL runtime·save_g(SB)
MOVD (g_sched+gobuf_sp)(g), R0
MOVD R0, RSP
MOVD (g_sched+gobuf_bp)(g), R29
MOVD $0, (g_sched+gobuf_sp)(g)
MOVD $0, (g_sched+gobuf_bp)(g)
RET
noswitch:
// already on m stack, just call directly
// Using a tail call here cleans up tracebacks since we won't stop
// at an intermediate systemstack.
MOVD 0(R26), R3 // code pointer
MOVD.P 16(RSP), R30 // restore LR
SUB $8, RSP, R29 // restore FP
B (R3)
3. runtime.newproc1
分三步:
- 获取g
优先从本地的空闲g列表获取。如果没有空闲,则创建一个g用来运行函数fn(对于主线程来说就是runtime.main()
,对于业务代码来说,就是go指定的函数)。 - 将g放入待执行队列
设置g状态为可运行_Gdead
–>_Grunnable
,将新创建的g放入等待运行队列。如果本地队列满了,则将本地队列一半的g放入全局队列。 - wakep()
只有当前有空闲的P,并且没有m处于自旋状态(没有任务,正在等待任务)。才会调用wakep()
。
只有当前没有处于自旋状态的m时,尝试将自旋个数从0修改为1,当多个线程并发时,只会有一个线程成功,然后才会调用startm(nil, true)
。
startm(nil, true)逻辑:
如果当前没有空闲的p,即sched.pidle
为空,则什么也不做。
如果找到一个空闲的p:- 如果没有空闲m,则新建一个m,将m与p关联,新建一个系统线程,执行goroutine函数。
- 如果有空闲m,则唤醒该m线程。
// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
// 1. 参数大小校验
if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
throw("newproc: function arguments too large for new goroutine")
}
// 2. 从本地、或者全局gfree列表获取g对象,并分配栈空间
_p_ := _g_.m.p.ptr()
newg := gfget(_p_)
// 3. 如本地、全局gfree都为空,则新创建一个g对象,并且分配栈空间。设置g状态_Gidle-->_Gdead。添加到全局g列表allgs。
if newg == nil {
newg = malg(_StackMin)
// newg := new(g)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg)
}
// 4. 原子读取g的状态,新的g对象状态必须是 _Gdead 状态,否则抛出异常。
// 5. 准备goroutine执行环节,将gobuf.pc设置为fn。
gostartcallfn(&newg.sched, fn)
// 保存调用者地址
newg.gopc = callerpc
// 6. 如果是系统goroutine,系统计数器+1.
// 7. 设置goroutine状态,_Gdead-->_Grunnable
casgstatus(newg, _Gdead, _Grunnable)
7.1.这里会循环等待将状态从_Gdead修改为_Grunnable。
7.2 如果状态直接由_Gdead修改为_Grunnable,则进程空执行10个CPU周期。
TEXT runtime·procyield(SB),NOSPLIT,$0-0
MOVL cycles+0(FP), AX
again:
PAUSE
SUBL $1, AX
JNZ again
RET
7.3 如果g当前状态不等于_Gdead,则调用系统线程调用,让出cpu等待状态的变更。
src\runtime\sys_linux_amd64.s
TEXT runtime·osyield(SB),NOSPLIT,$0
MOVL $SYS_sched_yield, AX
SYSCALL
RET
// 8. 如果开启了trace,则调用traceGoCreate
traceGoCreate(newg, newg.startpc)
// 9. 将g放入本地待执行队列。p.runq
runqput(_p_, newg, true)
9.1 如果开启了随机调度,randomizeScheduler,则进行随机选择(随机数模2==0)即,50%概率作为下一个执行的g。
9.2 如果可以作为下个执行的g,设置到p.runnext.
9.3 如果不能作为下个执行的g,将g放入p的待执行队列,p.runq
9.4 如果p.runq队列满了,则调用runqputslow,把g放到全局运行队列
runqputslow会把本地运行队列中一半的g放到全局运行队列。
// 10. 如果有线程空闲,则唤醒,没有则新建一个线程。
// Tries to add one more P to execute G's.
// Called when a G is made runnable (newproc, ready).
wakep()
添加一个或多个p来执行g。当g处于可执行状态时调用。
10.1 交换nmspinning为1,多线程并发时,只有一个线程会成功。
if !atomic.Cas(&sched.nmspinning, 0, 1) {
return
}
10.2 startm(nil, true)
1. 获取一个空闲的p,如果没有获取到什么也不做。
2. 从midle列表获取一个m。
3. 如果获取m失败,则新创建一个新的系统线程
newm(fn, _p_)
4. 如果获取到m,则唤醒线程
notewakeup(&mp.park)
}
3. gfget()
- 从本地空闲列表获取一个g对象,如果本地为空,全局非空,则从全局获取一批(最多32)个到本地列表。
- 给获取到的g分配栈空间(如果没有栈空间)。
// Get from gfree list.
// If local list is empty, grab a batch from global list.
func gfget(_p_ *p) *g {
retry:
// 1.如果本地的gfree为空,并且全局的栈上、或者非栈gfree不为空,则从全局gfree获取一批g对象(最多32个),并放入本地gfree列表。
// 过程需要对全局sched.gfree加锁。
if _p_.gFree.empty() && (!sched.gFree.stack.empty() || !sched.gFree.noStack.empty()) {
lock(&sched.gFree.lock)
// Move a batch of free Gs to the P.
for _p_.gFree.n < 32 {
// Prefer Gs with stacks.
gp := sched.gFree.stack.pop()
if gp == nil {
gp = sched.gFree.noStack.pop()
if gp == nil {
break
}
}
sched.gFree.n--
_p_.gFree.push(gp)
_p_.gFree.n++
}
unlock(&sched.gFree.lock)
goto retry
}
// 2. 从本地g空闲列表获取一个g对象
gp := _p_.gFree.pop()
if gp == nil {
return nil
}
_p_.gFree.n--
// 3. 如果g对象还未设置栈空间,则申请栈空间
if gp.stack.lo == 0 {
// Stack was deallocated in gfput. Allocate a new one.
systemstack(func() {
gp.stack = stackalloc(_FixedStack)
})
gp.stackguard0 = gp.stack.lo + _StackGuard
} else {
if raceenabled {
racemalloc(unsafe.Pointer(gp.stack.lo), gp.stack.hi-gp.stack.lo)
}
if msanenabled {
msanmalloc(unsafe.Pointer(gp.stack.lo), gp.stack.hi-gp.stack.lo)
}
}
return gp
}
4. 总结
- 通过go关键字创建一个goroutine.
- 创建goroutine时,是在g0,或者gsignal栈上执行的。
- 优先从本地(当前线程p对象)获取g对象,本地对象为空则从全局(sched)的gfree队列取一批(最多32个)到本地。
- 没有空闲g对象,则新创建一个g对象。
- 设置新的g对象的状态为_Grunnable。
- 如果开启随机执行,则以50%的概率将当前g作为下个执行对象p.runnext。
- 如果没有选为下个执行对象,则放入待执行队列(一个256大小数组
p.runq
)中,如果队列已经满了(最大256)个,则放入全局队列(同时会将当前p上一半的的待运行g放入全局队列sched.runq
)。 - 获取一个空闲的P,如果没有则什么也不做。
- 获取到空闲的P,则唤醒一个空闲M线程。如果没有空闲m,则新建一个m线程。