• 说说Golang的runtime


    https://zhuanlan.zhihu.com/p/27328476

    runtime包含Go运行时的系统交互的操作,例如控制goruntine的功能。还有debug,pprof进行排查问题和运行时性能分析,tracer来抓取异常事件信息,如 goroutine的创建,加锁解锁状态,系统调用进入推出和锁定还有GC相关的事件,堆栈大小的改变以及进程的退出和开始事件等等;race进行竞态关系检查以及CGO的实现。总的来说运行时是调度器和GC,也是本文主要内容。


    scheduler


    首先说到调度,我们学习操作系统时知道,对于CPU时间片的调度,是系统的资源分配策略,如任务A在执行完后,选择哪个任务来执行,使得某个因素(如进程总执行时间,或者磁盘寻道时间等)最小,达到最优的服务。这就是调度关注的问题。那么Go的运行时的scheduler是什么呢?我们为什么需要它,因为我们知道OS内核已经有一个线程(进程)scheduler了嘛?

    为什么Go还要自己搞一套?想想我们是不是经常说Go牛逼啊,语言级别实现了并发,我们为什么会这样说呢?愿意就在于此,Go有自己的scheduler。

    说了这么多,到底为什么?我们知道线程有自己的信号掩码,上下文环境以及各种控制信息等,但这些很多特征对于Go程序本身来说并不关心, 而且context上下文切换的耗时费时费力费资源,更重要的是GC的原因,也是本文下部分说的,就是Go的垃圾回收需要stop the world,所有的goroutine停止,才能使得内存保持在一个一致的状态。垃圾回收的时间会根据内存情况变化是不确定的,如果我们没有自己的scheduler我们交给了OS自己的scheduler,我们就失去了控制,并且会有大量的线程需要停止工作。所以Go就需要自己单独的开发一个自己使用的调度器,能够自己管理goruntines,并且知道在什么时候内存状态是一致的,也就是说,对于OS而言运行时只需要为当时正在CPU核上运行的那个线程等待即可,而不是等待所有的线程。
    每一个Go程序都附带一个runtime,runtime负责与底层操作系统交互,也都会有scheduler对goruntines进行调度。在scheduler中有三个非常重要的概念:P,M,G。

    查看源码/src/runtime/proc.go我们可以看到注释:
    // Goroutine scheduler
    // The scheduler's job is to distribute ready-to-run goroutines over worker threads.
    //
    // The main concepts are:
    // G - goroutine.
    // M - worker thread, or machine.
    // P - processor, a resource that is required to execute Go code.
    //     M must have an associated P to execute Go code, however it can be
    //     blocked or in a syscall w/o an associated P.
    //
    // Design doc at https://golang.org/s/go11sched.
    

    我们也看下Go程序的启动流程:
    // The bootstrap sequence is:
    //
    //	call osinit
    //	call schedinit
    //	make & queue new G
    //	call runtime·mstart
    //
    // The new G calls runtime·main.
    

    想要明白详细的流程可见:golang internals - Genius0101 - 博客园
    那么scheduler究竟解决了什么问题并如何管理goruntines呢?

    想要自己解决调度,避不开一个问题那就是栈的管理,也就是说每个goroutine都有自己的栈,在创建goroutine时,就要同时创建对应的栈。那么可知goroutine在执行时,栈空间会不停增长。 栈通常是连续增长的,每个进程中的各个线程共享虚拟内存空间,当有多个线程时,就需要为每个线程分配不同起始地址的栈,这就需要在分配栈之前先预估每个线程栈的大小。为了解决这个问题,就有了Split Stacks技术: 创建栈时,只分配一块比较小的内存,如果进行某次函数调用导致栈空间不足时,就会在其他地方分配一块新的栈空间。 新的空间不需要和老的栈空间连续。函数调用的参数会拷贝到新的栈空间中,接下来的函数执行都在新栈空间中进行。runtime的栈管理方式与此类似,但是为了更高的效率,使用了连续栈 (Golang连续栈) 实现方式也是先分配一块固定大小的栈,在栈空间不足时,分配一块更大的栈,并把旧的栈全部拷贝到新栈中,这样避免了Split Stacks方法可能导致的频繁内存分配和释放。

    既然要调度那么肯定要有自己的调度策略了,go使用抢占式调度,goroutine的执行是可以被抢占的。如果一个goroutine一直占用CPU,长时间没有被调度过, 就会被runtime抢占掉,把CPU时间交给其他goroutine。详见:Go Preemptive Scheduler Design Doc runtime在程序启动时,会自动创建一个系统线程,运行sysmon()函数, sysmon()函数在整个程序生命周期中一直执行,负责监视各个Goroutine的状态、判断是否要进行垃圾回收等,sysmon()会调用retake()函数,retake()函数会遍历所有的P,如果一个P处于执行状态, 且已经连续执行了较长时间,就会被抢占。

    然后retake()调用preemptone()将P的stackguard0设为stackPreempt,这将导致该P中正在执行的G进行下一次函数调用时, 导致栈空间检查失败,进而触发morestack(),在goschedImpl()函数中,会通过调用dropg()将G与M解除绑定;再调用globrunqput()将G加入全局runnable队列中;最后调用schedule() 来用为当前P设置新的可执行的G。

    如上图:go function 即可启动一个goroutine,所以每go出去一个语句被执行,runqueue队列就在其末尾加入一个goroutine,并在下一个调度点,就从runqueue中取出,一个goroutine执行。同时每个P可以转而投奔另一个OS线程,保证有足够的线程来运行所以的context P,也就是说goruntine可以在合适时机在多个OS线程间切换,也可以一直在一个线程,这由调度器决定。

    GC


    GC一直是Go开发团队一直在优化的地方,它的性能也越来越好:
    GC 1.5 vs 1.6
    GC1.7
    可能从图上我们不好看出变化:在 Go 1.4 版本的时候它的 GC 在 300 毫秒的时候,但是在 1.5 版本 GC 已经优化得非常好了,压缩到了40 毫秒。从 1.6 版本的 15 到 20 毫秒升级到 1.63 版本的 5 毫秒。又从 1.6.3 升级到 1. 7 版本的 3 毫秒以内,同样在刚发布的1.8版本中,GC在低延迟方面的优化又给了我们大的惊喜,由于消除了GC的“stop-the-world stack re-scanning”,使得GC STW(stop-the-world)的时间通常低于100微秒,甚至经常低于10微秒,现在 GC 已经不是他们的问题了。GC 降下来了,CPU 使用率就上去了,1.7.3 和 1.8 版本中,CPU 会多利用一些,CPU 的使用率相对上升了一点,但是 GC 有很大的提升,当然这或多或少是以牺牲“吞吐”作为代价的,因此在Go 1.9中,GC的改进将持续进行,会在吞吐和低延迟上做一个很好的平衡。应该说,在 1.8 版本发布之后,1.9 版本现在引入了一个理念——goroutine 级别的GC,所以 1.9 版本可能还有更大的提升。

    GC优化之路:
    1. 1.3 以前,使用的是比较蠢的传统 Mark-Sweep 算法。
    2. 1.3 版本进行了一下改进,把 Sweep 改为了并行操作。
    3. 1.5 版本进行了较大改进,使用了改进三色标记算法,叫做“非分代的、非移动的、并发的、三色的标记清除垃圾收集器”,go 除了标准的三色收集以外,还有一个辅助回收功能,防止垃圾产生过快。分为两个主要阶段-markl阶段:GC对对象和不再使用的内存进行标记;sweep阶段,准备进行回收。这中间还分为两个子阶段,第一阶段,暂停应用,结束上一次sweep,接着进入并发mark阶段:找到正在使用的内存;第二阶段,mark结束阶段,这期间应用再一次暂停。最后,未使用的内存会被逐步回收,这个阶段是异步的,不会STW。
    4. 1.6中,finalizer的扫描被移到了并发阶段中,对于大量连接的应用来说,GC的性能得到了显著提升。
    5. 1.7号称史上改进最多的版本,在GC上的改进也很显著:并发的进行栈收缩,这样我们既实现了低延迟,又避免了对runtime进行调优,只要使用标准的runtime就可以。
    6. 1.8 消除了GC的“stop-the-world stack re-scanning”

    Go的GC目前来说已经做的非常好了,未来在1.9将更多在GC优化下对于吞吐和效率的平衡,我们一起期待!


    参考文献:

    为Go语言GC正名-20秒到100微妙的演变史 - Sunface - 博客频道 - CSDN.NET
    [转]Golang中goroutine的调度器详解 - heiyeluren的blog(黑夜路人的开源世界) - 博客频道 - CSDN.NET
  • 相关阅读:
    管理经济学之第三章(消费者效用分析)
    管理经济学之第二章(供求分析)
    JVM之GC回收信息详解
    管理经济学之第一章(导论)
    JAVA的引用类型
    6.jQuery动画和队列,简单的queue()入队和dequeue()出队实现
    5.jQuery实现简单的on()和trigger()方法
    4.jQuery的clone()方法和data()方法
    3.jQuery操作DOM对象的方法
    2.jQuery简单实现get()和eq()和add()和end()方法
  • 原文地址:https://www.cnblogs.com/dhcn/p/13671722.html
Copyright © 2020-2023  润新知