• goroutine切换背后那些事儿


    本文基于于GoLang 1.13。

    1. 写在前面

    微信公众号:[double12gzh]

    个人主页: https://gzh.readthedocs.io

    关注容器技术、关注Kubernetes。问题或建议,请公众号留言。

    Goroutine很轻量,从资源消耗方面来看,它只需要一个2Kb的内存栈就可以运行;从运行时来看,它的运行成本也很低,将一个goroutine切换到另一个goroutine并不需要很多操作。

    在进行讲解golang的切换之前,我们先High Level的看一下goroutine切换的相关内容。

    2. 案例

    golang会根据两种断点将goroutine调度到线程上:

    • 当一个goroutine阻塞了。如:系统调用,mutex,或者通道。被阻塞的goroutine会进入睡眠模式/队列,让Go调度并运行一个等待的goroutine。被阻塞的goroutine进入睡眠模式/队列,允许Go调度和运行一个等待的goroutine。

    • 在函数调用过程中,假如goroutine必须增长它的栈。这个断点允许Go调度另一个goroutine,避免正在运行的那个goroutine占用CPU。

    在这两种情况下,运行调度器的g0会用另一个准备运行的goroutine替换当前的goroutine。然后,被选中的goroutine取代g0,从而在线程上运行。

    如果您想了解更多关于g0的内容,请参考g0

    将一个运行中的goroutine切换到另一个运行中的goroutine涉及到两个切换。

    • g->g0

    • g0-> 另一个g

    在GoLang中,groutine真的非常轻量。为了保存,它只需要两个东西:

    • goroutine是在哪一行停止的。即:在被调度前,goroutine是在哪一行停止的,当前要运行的指令被记录在程序计数器(PC)中。goroutine稍后将在同一点恢复。

    • 存放goroutine的堆栈。这个堆栈的目的是为了方便再次运行时恢复其局部变量。

    下面我们深入看一下。

    3. PC(程序记数器)

    为了便于举例,我将使用一个通过channel进行通信的goroutine来说明,这两个goroutine中,一个可以产生数据的,其它的用于消费数据。代码如下:

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    const COUNT = 100
    
    func main() {
    	var wg sync.WaitGroup
    
    	c := make(chan int, 10)
    
    	wg.Add(1)
    
    	// 生产数据
    	go func() {
    		for i := 0; i < COUNT; i++ {
    			c <- i
    		}
    
    		close(c)
    		wg.Done()
    	}()
    
    	// 消费数据
    	for i := 0; i < 3; i++ {
    		wg.Add(1)
    
    		go func() {
    			for v := range c {
    				if v%2 == 0 {
    					fmt.Println(v)
    				}
    			}
    		}()
    	}
    
    	wg.Wait()
    }
    

    消费者基本上会打印0到99的偶数,我们将重点关注第一个goroutine--生产者--向缓冲区添加数字。当缓冲区满了,它将在发送消息时阻塞。此时,Go要切换到g0,调度另一个goroutine。

    如前所述,Go首先需要保存当前指令,以便在同一指令处恢复goroutine。程序计数器(PC)保存在goroutine的内部结构中。

    上面的代码可以使用以下图来简单说明:

    指令和它们的地址可以通过命令获取:

    ➜  hello go tool compile -N -l main.go
    ➜  hello ls | grep main.o
    main.o
    

    下面是生产者的一个示例:

    ➜  hello go tool objdump main.o
    

    在函数runtime.chansend1上阻塞通道前,程序逐条指令执行。Go将当前的程序计数器保存到当前goroutine的内部属性中。在我们的例子中,Go保存程序计数器的地址是0x4268d0,这个地址是在runtime和方法runtime.chansend1内部的。

    然后,当g0唤醒goroutine时,它将在同一指令处恢复,对数值进行循环并推入通道。

    下面我们来谈谈goroutine切换过程中的栈管理。

    4. 栈(stack)

    在被阻塞之前,正在运行的goroutine有它的原始栈。这个堆栈包含临时内存,比如变量i:

    然后,当它在通道上阻塞时,goroutine将和它的堆栈一起切换到g0,这个goroutine将会有一个更大的栈。

    在切换之前,堆栈将被保存,以便在goroutine再次运行时恢复。

    我们现在已经完整地了解了goroutine切换中涉及的不同操作。现在让我们看看它是如何影响性能的。

    我们应该注意到,一些架构(比如arm)需要多保存一个寄存器LR(链接寄存器)。

    5. 操作

    为了测量goroutine切换可能需要的时间,我们将使用前面写的程序。然而,它并不能给出一个完美的性能视图,因为它可能取决于找到下一个要调度的goroutine所需的时间。这样goroutine的切换也会影响性能,从函数prolog的切换比从通道上阻塞的goroutine切换要做的操作更多。

    我们来总结一下我们要测量的操作:

    • 当前的g在通道上阻塞并切换到g0:
      • PC和堆栈指针一起被保存在一个内部结构中
      • g0被设置为正在运行的goroutine。
      • g0的堆栈取代了当前的堆栈。
    • g0正在寻找一个新的goroutine来运行。
    • g0必须与所选的goroutine进行切换。
      • PC和堆栈指针被从内部结构中提取出来。
      • 程序跳转到获取的PC地址。

    如下图:

    gg0g0到g的切换是最快的阶段。它们包含少量固定的指令,这一点与调度器检查许多源以寻找下一个要运行的goroutine的情况相反。根据运行程序的情况,这个阶段甚至可能需要更多的时间。

    需要说明的一点是,对于以上测试的结果会因机器架构的不同而不同


    欢迎关注我的微信公众号:

  • 相关阅读:
    vue2 v-model/v-text 中使用过滤器的方法示例
    HTML5游戏开发案例教程合集
    Docker实战案例视频课程
    Java项目框架架构与优化教程
    Linux云计算-虚拟化技术视频教程
    udl
    Chloe官网及基于NFine的后台源码毫无保留开放
    抽象类存在的意义和作用
    Shell 脚本语法
    Github 高级搜索功能
  • 原文地址:https://www.cnblogs.com/double12gzh/p/13709055.html
Copyright © 2020-2023  润新知