在高并发中,如果去频繁的创建线程会产生不必要的开销,所以有了线程池,它可以预先保存一定数量的线程,新的任务不必再去创建线程,而是将任务发布到任务队列,线程池中的线程不断的从任务队列中取出任务并执行,这样可以有效的减少线程创建和销毁所带来的开销。
如上图,我们把任务队列中的每个任务称为 G ,G 往往代表一个函数。线程池中的 worker 线程不断的从任务队列中取出任务执行,worker 线程的调度是由操作系统来进行调度的。
若 worker 线程执行的 G 任务中发生系统调用,则操作系统会将该线程置为阻塞状态,意味着该线程在怠工、消费任务队列的 worker 线程变少了,也就是说线程池消费任务队列的能力变弱了。
如果任务队列中的大部分任务都进行系统调用,大部分 worker 线程进入阻塞状态,导致任务队列中的任务产生堆积。
解决这个问题的一个思路就是重新审视线程池中线程的数量,增加线程池中线程数量可以一定程度上提高消费能力,但随着线程数量增多,过多线程会争抢 CPU,消费能力会有上限,甚至出现消费能力下降。如下图所示:
线程数过多,那么操作系统会频繁的切换线程,频繁的上下文切换就成了性能瓶颈。Go 可以在线程中自己实现调度,上下文切换可以更轻量,达到了线程数少,而并发数并不少的效果。而线程中调度的就是 Goroutine.
Goroutine主要概念如下:
G(Goroutine): 即 Go 协程,每个 go 关键字都会创建一个协程。M(Machine):工作线程,在 Go 中称为 Machine。P(Processor): 处理器(Go中定义的一个概念,不是指CPU),包含运行 Go 代码的必要资源,也有调度 goroutine 的能力。
其关系如下图所示:
如上图:
上图中可见每个 P 维护着一个包含 G 的队列,不考虑 G 进入系统调用或 IO 操作的情况下,P 周期性的将 G 调度到 M 中执行,执行一小段时间,将上下文保存下来,然后将 G 放到队列尾部,然后从队列中重新取出一个G进行调度。
除了每个 P 维护的 G 队列以外,还有一个全局的队列。每个 P 会周期性的查看全局队列中是否有 G 待运行并将其调度到 M 中执行,全局队列中 G 的来源,主要有从系统调用中恢复的 G。之所以 P 会周期性的查看全局队列,也是为了防止全局队列中的 G 被“饿死”。
前面说到 P 的个数默认是 CPU 核数,每个 M 必须持有一个 P 才可以执行 G,一般 M 的个数会略大于 P 的个数,多出来的 M 会在 G 产生系统调用时发挥作用。类似线程池,Go 也提供一个 M 的池子,需要时从池子中获取,用完放回池子,不够时就再创建一个。
当 M 运行的某个 G 产生系统调用时,如下图所示:
如图,当 G0 即将进入系统调用时,M0 将释放 P,进而某个空闲的 M1 获取 P,继续执行 P 队列中剩下的 G。而 M0 由于陷入系统调用而被阻塞,M1 接替 M0 的工作,只要 P 不空闲,就可以保证充分利用 CPU。
M1 可能是来自 M 的缓存池,也可能是新建的。当 G0 系统调用结束后,根据 M0 是否能获取到 P,会将 G0 做不同的处理:
多个 P 中维护的 G 队列有可能是不均衡的,比如下图:
另一种情况是 P 所分配的任务 G 很快就执行完了(分配不均),这就导致了这个处理器 P 很忙,但是其他的 P 还有任务,此时如果 global runqueue 没有任务 G 了,那么 P 不得不从其他的 P 里拿一些 G 来执行。一般来说,如果 P 从其他的 P 那里要拿任务的话,一般就拿 run queue 的一半,这就确保了每个 OS 线程都能充分的使用,如上图。
M和P的数量如何确定?或者说何时会创建M和P?
由启动时环境变量
GOMAXPROCS个goroutine在同时运行。
go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。runtime/debug中的SetMaxThreads函数,设置M的最大数量 一个M阻塞了,会创建新的M。M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。
一般,GOMAXPROCS 的大小设置为 CPU 的核数,使 Go 程序能充分利用 CPU。在 IO 密集型的应用里,这样设置可能性能并不是最好。理论上讲当某个 Goroutine 进入系统调用时,会有一个新的 M 被启用或创建,继续占满 CPU。但 Go 旧的 M 被阻塞和新的 M 得到运行之间是有一定间隔的(延迟),所以在 IO 密集型应用中可以把 GOMAXPROCS 设置大一些,效果或许会更好。
加我微信,拉你进技术交流群:wucs_dd