goroutine基本介绍
Go主线程和携程
一个Go主线程上,可以有多个协程,协程是轻量级线程
主线程一旦退出,其他协程都会被杀死
为什么提出goroutine
线程数过多,意味着操作系统会不断地切换线程, 频繁的上下文切换就成了性能瓶颈
Go提供一种机制,可以在线程中自己实现调度,上下文切换更轻量,从而达到了线程数少,而并发数并不少的效果。而线程中调度的就是Goroutine(也就是协程)
Go协程特点
- 有独立的栈空间:而goroutines为了避免资源浪费(亦或是资源缺乏),采用动态扩张收缩的策略,初始量为2k,最大可以扩张到1G
- 共享程序堆空间
- 调度由用户控制(用户级):因为协程在用户态由协程调度器完成,不需要陷入内核,切换调度开销小(协程只修改3个寄存器 - PC/SP/DX)
- 协程是轻量级线程
goroutine调度模型
调度机制用一句话描述:runtime 准备好 M、P、G,然后 M 绑定 P,M 从各种队列(本地队列、全局队列)中获取 G,切换到 G 的执行栈上并执行 G 的任务函数,条用 goexit 做清理工作并回到 M,如此反复。
MPG模式基本介绍
- M(Machine):
- 面向操作系统的线程,即内核线程。
- M 是真正调度系统的执行者,每一个 M 就像一个勤劳的工作者,总是从各种队列中找到可运行的 G,这样的 M 可以有多个(多线程)。
- M 在绑定有效的 P 后,进入调度循环,而且 M 并不保留 G 状态,这个 G 可以跨 M 调度的基础。
- P(Processor):
- P 表示逻辑 Processor,是协程执行需要的上下文(不只是处理器)。
- P 的最大作用是其拥有的各种 G 对象队列、链表、cache 和状态。
- G(Goroutine):
- 调度系统中最基本的单位,存储了 goroutine 的栈信息、状态、任务函数等。
- 在 G 眼中只有 P,P是运行 G 的 ”CPU“。
M是交给操作系统调度的线程,M持有一个P,P将G调度进入M中执行。
P同时还维护着一个包含G的队列,可以按照一定的策略将G调度到M中执行。
P的个数在程序启动时已经确定,默认情况下等同于CPU的核心数。由于M必须持有一个P才可以运行,所以同时运行的M个数,也即线程数一般略大于CPU的个数(多出来的M用于系统调用),以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。
设置运行时CPU数目 - runtime包
1 | func NumCPU() int |
NumCPU
返回本地机器的逻辑CPU个数。
1 | func GOMAXPROCS(n int) int |
GOMAXPROCS
设置可同时执行的最大CPU数(P 的个数),并返回先前的设置。 若 n < 1
,它就不会更改当前设置。本地机器的逻辑CPU数可通过 NumCPU
查询。本函数在调度程序优化后会去掉(Go 1.8以后)。
调度策略
每个P维护一个包含G的队列,采用非抢占式轮转算法进行调度。
每个P除了维护一个包含G的队列外,还有一个全局队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。
对于新创建的 G,会被放置到全局可执行 G 队列中,等待调度器分发到合适的 P 的可执行 G 队列中。
系统调用
P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。
每一次进行系统调用,比如M0下的G0进入系统调用,那么M0会释放P,进而某个空闲的M1就可以获取到P继续执行P队列剩下的G。
进行系统调用时,P 将会断开与 M 的联系。保证 P 中可执行 G 队列中其他 G 得到执行,且由于程序中并行执行的 M 数量没变,保证了程序 CPU 的高利用率。
M0由于陷入系统调用被阻塞,M1获得了P(只要P不空闲),可以代替M0的工作,保证充分利用CPU。M1的来源可以是M的池子,也可以是新建的。当系统调用结束后,M0根据能否获取到P,将对G0(执行系统调用的G)做不同的处理:
- 如果有空闲的P,则获取一个P,继续执行G0
- 没有空闲的P,则将G0放入全局队列,等待被其他P调度。M0进入缓存池子休眠
工作量窃取
多个P维护的G队列可能是不平衡的,若一个P的G队列的全部工作早早完成,就去查询全局队列。若全局队列也没有G,但是另一个P维护的队列中有G。此时,空闲的P会将其他P总的G窃取一部分过来,一般是窃取一半。