文档章节

Go goroutine调度

恋恋美食
 恋恋美食
发布于 2018/09/29 12:25
字数 1981
阅读 628
收藏 7
Go

前言

Goroutine调度是一个很复杂的机制,尽管Go源码中提供了大量的注释,但对其原理没有一个好的理解的情况下去读源码收获不会很大。下面尝试用简单的语言描述一下Goroutine调度机制,在此基础上再去研读源码效果可能更好一些。

1. 线程池的缺陷

我们知道,在高并发应用中频繁创建线程会造成不必要的开销,所以有了线程池。线程池中预先保存一定数量的线程,而新任务将不再以创建线程的方式去执行,而是将任务发布到任务队列,线程池中的线程不断的从任务队列中取出任务并执行,可以有效的减少线程创建和销毁所带来的开销。

下图展示一个典型的线程池:

为了方便下面的叙述,我们把任务队列中的每一个任务称作G,而G往往代表一个函数。
线程池中的线程worker线程不断的从任务队列中取出任务并执行。而worker线程的调度则交给操作系统进行调度。

如果worker线程执行的G任务中发生系统调用,则操作系统会将该线程置为阻塞状态,也意味着该线程在怠工,也意味着消费任务队列的worker线程变少了,也就是说线程池消费任务队列的能力变弱了。

如果任务队列中的大部分任务都会进行系统调用,则会让这种状态恶化,大部分worker线程进入阻塞状态,从而任务队列中的任务产生堆积。

解决这个问题的一个思路就是重新审视线程池中线程的数量,增加线程池中线程数量可以一定程度上提高消费能力,但随着线程数量增多,由于过多线程争抢CPU,消费能力会有上限,甚至出现消费能力下降。 如下图所示:

2. Goroutine调度器

线程数过多,意味着操作系统会不断的切换线程,频繁的上下文切换就成了性能瓶颈。 Go提供一种机制,可以在线程中自己实现调度,上下文切换更轻量,从而达到了线程数少,而并发数并不少的效果。而线程中调度的就是Goroutine.

早期Go版本,比如1.9.2版本的源码注释中有关于调度器的解释。
Goroutine 调度器的工作就是把“ready-to-run”的goroutine分发到线程中。

Goroutine主要概念如下:

  • G(Goroutine): 即Go协程,每个go关键字都会创建一个协程。
  • M(Machine): 工作线程,在Go中称为Machine。
  • P(Processor): 处理器(Go中定义的一个摡念,不是指CPU),包含运行Go代码的必要资源,也有调度goroutine的能力。

M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。其关系如下图所示:

图中M是交给操作系统调度的线程,M持有一个P,P将G调度进M中执行。P同时还维护着一个包含G的队列(图中灰色部分),可以按照一定的策略将不能的G调度进M中执行。

P的个数在程序启动时决定,默认情况下等同于CPU的核数,由于M必须持有一个P才可以运行Go代码,所以同时运行的M个数,也即线程数一般等同于CPU的个数,以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。

程序中可以使用runtime.GOMAXPROCS()设置P的个数,在某些IO密集型的场景下可以在一定程度上提高性能。这个后面再详细介绍。

3. Goroutine调度策略

3.1 队列轮转

上图中可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性的查看全局队列中是否有G待运行并将期调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性的查看全局队列,也是为了防止全局队列中的G被饿死。

3.2 系统调用

上面说到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做不同的处理:

  1. 如果有空闲的P,则获取一个P,继续执行G0。
  2. 如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。

3.3 工作量窃取

多个P中维护的G队列有可能是不均衡的,比如下图:

竖线左侧中右边的P已经将G全部执行完,然后去查询全局队列,全局队列中也没有G,而另一个M中除了正在运行的G外,队列中还有3个G待运行。此时,空闲的P会将其他P中的G偷取一部分过来,一般每次偷取一半。偷取完如右图所示。

4. GOMAXPROCS设置对性能的影响

一般来讲,程序运行时就将GOMAXPROCS大小设置为CPU核数,可让Go程序充分利用CPU。
在某些IO密集型的应用里,这个值可能并不意味着性能最好。 理论上当某个Goroutine进入系统调用时,会有一个新的M被启用或创建,继续占满CPU。 但由于Go调度器检测到M被阻塞是有一定延迟的,也即旧的M被阻塞和新的M得到运行之间是有一定间隔的,所以在IO密集型应用中不妨把GOMAXPROCS设置的大一些,或许会有好的效果。

5.参考文章

5.1 《The Go scheduler》http://morsmachine.dk/go-scheduler

赠人玫瑰手留余香,如果觉得不错请给个赞~

本篇文章已归档到GitHub项目,求星~ 点我即达

© 著作权归作者所有

恋恋美食

恋恋美食

粉丝 110
博文 155
码字总数 140004
作品 0
杭州
高级程序员
私信 提问
Goroutine调度实例简要分析

Goroutine调度实例简要分析 Tony Bai2017-11-235 阅读 githubAssemblyGolangGCGoCompiler 前两天一位网友在 微博 私信我这样一个问题: 抱歉打扰您咨询您一个关于 Go 的问题:对于goroutine...

Tony Bai
2017/11/23
0
0
Go work-stealing 调度器

本文译自 Rakyll 的 scheduler版权@归原文所有. Go 调度器的工作是将可运行的 goroutine 分发到一个或多个处理器上运行的多个操作系统工作线程. 在多线程计算里, 调度出现了两种模式: work-...

A Coder @ Work
2018/01/31
0
0
goroutine的调度

goroutine的调度 概述 Go 1.1 重要特性之一就是由 Dmitry Vyukov 贡献的新调度器。无需对程序进行任何调整,新的调度器就可以为 Go 程序带来令人兴奋的性能提升。因此我觉得有必要就此写点什...

秋风醉了
2016/09/23
64
0
Golang goroutine与调度器

我们都知道Go语言是原生支持语言级并发的,这个并发的最小逻辑单元就是goroutine。goroutine就是Go语言提供的一种用户态线程,当然这种用户态线程是跑在内核级线程之上的。当我们创建了很多的...

吃一堑消化不良
2017/01/04
131
1
Go基础编程:并发编程—goroutine

1 goroutine是什么 goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的...

tennysonsky
2018/01/15
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Executor线程池原理与源码解读

线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。 线程实现方式 Thread、Runnable、Callable //实现Runnable接口的...

小强的进阶之路
昨天
6
0
maven 环境隔离

解决问题 即 在 resource 文件夹下面 ,新增对应的资源配置文件夹,对应 开发,测试,生产的不同的配置内容 <resources> <resource> <directory>src/main/resources.${deplo......

之渊
昨天
8
0
详解箭头函数和普通函数的区别以及箭头函数的注意事项、不适用场景

箭头函数是ES6的API,相信很多人都知道,因为其语法上相对于普通函数更简洁,深受大家的喜爱。就是这种我们日常开发中一直在使用的API,大部分同学却对它的了解程度还是不够深... 普通函数和...

OBKoro1
昨天
7
0
轻量级 HTTP(s) 代理 TinyProxy

CentOS 下安装 TinyProxy yum install -y tinyproxy 启动、停止、重启 # 启动service tinyproxy start# 停止service tinyproxy stop# 重启service tinyproxy restart 相关配置 默认...

Anoyi
昨天
2
0
Linux创建yum仓库

第一步、搞定自己的光盘 #创建文件夹 mkdir -p /media/cdrom #挂载光盘 mount /dev/cdrom /media/cdrom #编辑配置文件使其永久生效 vim /etc/fstab 第二步,编辑yun源 vim /ect yum.repos.d...

究极小怪兽zzz
昨天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部