zoukankan      html  css  js  c++  java
  • go goroutine调度机制 && goroutine池



    摘要

    使用go语言写程序差不多有半年多了,也对go语言有了更深的理解,今天聊聊go goroutine的调度原理。

    线程

    进程:进程是并发执行程序在执行过程中资源分配和管理的基本单位(资源分配的最小单位)。进程可以理解为一个应用程序的执行过程,应用程序一旦执行,就是一个进程。每个进程都有自己独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。

    线程:程序执行的最小单位。

    并发编程的目的是为了让程序运行得更快,但是并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题。

    上下文切换

    即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。

    CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换

    这就像我们同时读两本书,当我们在读一本英文的技术书籍时,发现某个单词不认识,于是便打开中英文词典,但是在放下英文书籍之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。

    在高并发应用中频繁创建线程会造成不必要的开销,所以有了线程池。

    线程池

    线程池中预先保存一定数量的线程,而新任务将不再以创建线程的方式去执行,而是将任务发布到任务队列,线程池中的线程不断的从任务队列中取 出任务并执行,可以有效的减少线程创建和销毁所带来的开销。

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

    image-20210210113625319

    G往往代表一个函数。线程池中的线程worker线程不断的从任务队列中取出任务并执行。而worker线程的调度则交给操作系统进行调度。

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

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

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

    image-20210210113720144

    这个问题如何解呢?

    Goroutine调度器

    线程数过多,意味着操作系统会不断的切换线程,频繁的上下文切换就成了性能瓶颈。Go提供一种机制,可以在线程中自己实现调度,上下文切换更轻量,从而达到了线程数少,而并发数并不少的效果。而线程中调度的就是 Goroutine.
    Goroutine 调度器的工作就是把“ready-to- run”的goroutine分发到线程中。
    Goroutine主要概念如下:

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

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

    image-20210218111044903

    图中M是交给操作系统调度的线程,M持有一个P,P将G调度进M中执行。P同时还维护着一个包含G的队列(图中灰色部 分),可以按照一定的策略将不能的G调度进M中执行。
    P的个数在程序启动时决定,默认情况下等同于CPU的核数,由于M必须持有一个P才可以运行Go代码,所以同时运行的 M个数,也即线程数一般等同于CPU的个数,以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。
    程序中可以使用 runtime.GOMAXPROCS() 设置P的个数,在某些IO密集型的场景下可以在一定程度上提高性能。

    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的池子,需要时从池子中获取,用完放回池 子,不够用时就再创建一个。

    image-20210218112642428

    如图所示,当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将进入缓存池睡眠

    工作量窃取

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

    image-20210218145622418

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

    小结

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

    Goroutine池

    同理,在写 go 并发程序的时候如果程序会启动大量的 goroutine ,势必会消耗大量的系统资源(内存,CPU),同理如果引入池化技术,衍生出goroutine池,复用 goroutine ,则会节省资源,提升性能。

    选择一个开源的Ants为例。

    ants运行原理

    性能

    从该Ants demo 测试吞吐性能对比可以看出,使用ants的吞吐性能相较于原生 goroutine 可以保持在 2-6 倍的性能压制,而内存消耗则可以达到 10-20 倍的节省优势。

    想要深入了解Ants,请移步项目地址:https://github.com/panjf2000/ants/blob/master/README_ZH.md

    参考

    The Go scheduler

    多线程上下文切换


    你的鼓励也是我创作的动力

    打赏地址

    微博:https://weibo.com/yangsanchao
  • 相关阅读:
    快速排序算法C++实现[评注版]
    浮躁的程序员
    扬长避短使用Windbg和Visual Studio高效调试调试你的代码
    程序员,代码,理想,老男孩
    Windows Server 2008 R2 如何启动kernel dbg进行双机内核调试『续bcdedit 用法详解』
    Windows Server 2008 R2 如何启动kernel dbg进行双机内核调试『配置详解』
    忙着活或忙着死[转]
    SQL2005使用游标的实例(SBO中计算到期应收账款)
    C#编写的Windows计算器源代码
    请登录真正的BBS
  • 原文地址:https://www.cnblogs.com/yangsanchao/p/14412111.html
Copyright © 2011-2022 走看看