13-goroutine创建过程

22 篇文章 1 订阅
2 篇文章 0 订阅

1. 创建一个goroutine

通过go创建一个goroutine。底层通过runtime.newproc()创建。
创建一个goroutine包括两部分:

  1. 创建一个g对象
    优先从g空闲列表获取一个,没有则新建。将该g放入待执行队列(本地,或者全局)。
  2. 执行该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。

  1. 获取调用者地址,用来返回。
  2. 通过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

切换到系统栈,然后调用传入函数。

  1. g0是线程绑定的。
  2. 如果是g0,或者gsignal goroutine,则直接调用调用传入的函数。
  3. 否则,先切换到g0栈,再执行传入函数。
  4. 执行完毕,切换回调用者栈。
// 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

分三步:

  1. 获取g
    优先从本地的空闲g列表获取。如果没有空闲,则创建一个g用来运行函数fn(对于主线程来说就是runtime.main(),对于业务代码来说,就是go指定的函数)。
  2. 将g放入待执行队列
    设置g状态为可运行_Gdead–>_Grunnable,将新创建的g放入等待运行队列。如果本地队列满了,则将本地队列一半的g放入全局队列。
  3. wakep()
    只有当前有空闲的P,并且没有m处于自旋状态(没有任务,正在等待任务)。才会调用wakep()
    只有当前没有处于自旋状态的m时,尝试将自旋个数从0修改为1,当多个线程并发时,只会有一个线程成功,然后才会调用startm(nil, true)
    startm(nil, true)逻辑:
    如果当前没有空闲的p,即sched.pidle为空,则什么也不做。
    如果找到一个空闲的p:
    1. 如果没有空闲m,则新建一个m,将m与p关联,新建一个系统线程,执行goroutine函数。
    2. 如果有空闲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()

  1. 从本地空闲列表获取一个g对象,如果本地为空,全局非空,则从全局获取一批(最多32)个到本地列表。
  2. 给获取到的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. 总结

  1. 通过go关键字创建一个goroutine.
  2. 创建goroutine时,是在g0,或者gsignal栈上执行的。
  3. 优先从本地(当前线程p对象)获取g对象,本地对象为空则从全局(sched)的gfree队列取一批(最多32个)到本地。
  4. 没有空闲g对象,则新创建一个g对象。
  5. 设置新的g对象的状态为_Grunnable。
  6. 如果开启随机执行,则以50%的概率将当前g作为下个执行对象p.runnext。
  7. 如果没有选为下个执行对象,则放入待执行队列(一个256大小数组p.runq)中,如果队列已经满了(最大256)个,则放入全局队列(同时会将当前p上一半的的待运行g放入全局队列sched.runq)。
  8. 获取一个空闲的P,如果没有则什么也不做。
  9. 获取到空闲的P,则唤醒一个空闲M线程。如果没有空闲m,则新建一个m线程。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值