Dawn's Blogs

分享技术 记录成长

0%

GO学习笔记 (3) goroutine

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而又不至于产生过多的线程切换开销。

img

设置运行时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窃取一部分过来,一般是窃取一半