Go语言协程浅析

一、进程与线程

进程是资源分配的基本单位,它是程序运行的实例,在程序运行时创建;线程是程序执行的最小单位,是进程的一个执行流,一个进程由多个线程组成的,这些线程并发执行并共享进程的内存等资源。

开启一个进程的开销比一个线程大得多,进程具有独立的内存空间,这使得进程间通信更加困难。

操作系统调度到CPU中执行的最小单位是线程。

发生线程上下文切换时,需要从操作系统用户态切换到内核态,记录上一个线程的重要寄存器值(如SP)、进程状态等信息,并存储在操作系统的进程控制块(TCB)中。

二、协程

Go语言协程是轻量级的线程。

协程是用户态的,协程的管理依赖Go语言运行时提供的调度器,并且Go语言的协程是从属于某个线程。协程与线程的对应关系为M:N。

Go语言协程切换只需保留极少状态和寄存器的值(SP/BP/PC),而线程切换会保留额外的寄存器变量值。

线程的调度在大部分时间是抢占式的,而GO语言中的协程在一般情况下是协作式的,当一个协程处理完自己的任务后,会主动将执行权让渡给其他协程。

线程的栈默认大小为2M,Go语言的协程栈默认为2KB。

在多核场景下,Go语言的协程是并发与并行同时存在的。

三、GMP模型

Go进程中的协程依托于线程,借助操作系统将线程调度到CPU执行,从而最终执行协程。

  • G — 表示 Goroutine,每一个 Goroutine 都包含堆栈、指令指针和其他用于调度的重要信息;
  • M — 表示操作系统的线程,它是被操作系统管理的线程;
  • P — 表示调度的上下文,它可以被看做一个运行于线程 M 上的本地调度器。

在某一时刻,一个P可能包含多个G,同时一个P在任一时刻只能绑定一个M。同时,一个G并不是固定绑定同一个P的,同样P绑定哪一个M也不固定。

请添加图片描述

每个线程中都有一个特殊的协程G0,其作用是执行协程调度的一系列运行时代码,而一般的协程用于执行用户代码。协程经历G→G0→G的过程完成一次循环调度。协程上下文切换要保存当前执行现场,并存储在g.gobuf结构体中,其中主要保存了几个cup的寄存器值rsp,rip,rbp。为了避免栈溢出,协程G0的栈会重复使用。

四、协程调度

1.调度器的生命周期

请添加图片描述

2.调度流程

请添加图片描述

在调度前,会检查程序是否处于垃圾回收阶段,如果是则检查是否需要执行后台标记协程。

Go语言的运行队列分为局部运行队列和全局运行队列。局部运行队列是一个长为256的环形队列。P的内部有一个runnext字段来标识下一个要执行的协程,如果不为空,则直接执行当前runnext指向的协程,而不去运行队列中寻找。

每个P的局部运行队列获取不到协程时,会从全局队列中获取。为了避免全局队列中的协程无法被执行的情况,Go语言调度器规定P每执行61次调度,会优先从全局队列中获取一个G到当前P中。

如果全局队列也为空,则会尝试从其他P中窃取协程。如果窃取不到任务,当前的P会与M解绑,放入空闲P队列,与P绑定的M进入休眠。

在P获取局部运行队列时,为了防止其他P窃取任务造成同时访问的情况,会在访问时加锁。

如果本地队列满了,调度器会将本地运行队列的一半放入全局运行队列,这保证了每个协程都有执行的机会。

协程窃取时会将P本地运行队列的一半放入自己的运行队列中。

3.调度时机

主动调度

Go语言的协程可以通过runtime.Gosched主动让渡自己的执行权。其核心代码位于goschedImpl函数中,主动调度需要先从当前协程切换到G0,取消G与M的关系,把G放入全局队列,然后开始新一轮调度。

func goschedImpl(gp *g) {
	status := readgstatus(gp)
	if status&^_Gscan != _Grunning {
		dumpgstatus(gp)
		throw("bad g status")
	}
	casgstatus(gp, _Grunning, _Grunnable)
    // 取消G与M的关系
	dropg()
	lock(&sched.lock)
    // 把G放入全局队列
	globrunqput(gp)
	unlock(&sched.lock)
	// 开始下一轮调度
	schedule()
}
被动调度

被动调度是指在休眠、通道阻塞、网络I/O阻塞、执行垃圾回收而暂停时,被动让渡自己的执行权。被动调度需要先从当前协程切换到G0,更新协程状态并解绑与M的关系,然后重新调度。如果当前协程被唤醒,会将协程的状态从 _Gwaiting 转换成 _Grunnable,并添加到P的本地运行队列。

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
	...
	mcall(park_m)
}

// park continuation on g0.
func park_m(gp *g) {
	...
	casgstatus(gp, _Grunning, _Gwaiting)
    // 取消G与M的关系
	dropg()

	if fn := _g_.m.waitunlockf; fn != nil {
		ok := fn(gp, _g_.m.waitlock)
		_g_.m.waitunlockf = nil
		_g_.m.waitlock = nil
		...
	}
	schedule()
}
抢占调度

在Go1.14中,当前协程的执行时间超过了10ms,则需要抢占。

Go1.14之前的抢占调度发生在执行函数调用阶段,Go1.14以后引入了信号抢占机制,使用SIGURG作为抢占信号。
参考:
万变不离其宗——浅析GoLang的GMP协程调度模型与channel通信机制
GMP 线程调度模型

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值