zoukankan      html  css  js  c++  java
  • golang scheduler工作窃取

    pic-0

    写在前面

    微信公众号:[double12gzh]

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

    本文基于GoLang 1.13

    在Go中创建goroutine很容易,也很快速。然而,Go最多只能在每个内核上同时运行一个,并且需要一种方法来存放其他goroutine,并确保负载在各处理器上得到很好的平衡。

    Goroutine队列

    Go通过本地队列全局队列在两个层面管理处于等待状态的goroutine。局部队列与每个处理器相连,而全局队列是唯一的,可以被所有处理器使用。

    pic-1

    每个本地队列的最大容量为256个,之后任何新进入的goroutine都会被推送到全局队列中。下面这个例子中的程序,可以产生成千上万的goroutine。

    package main
    
    import (
        "runtime"
    	"sync"
    )
    
    const COUNT = 3000
    
    func main() {
        runtime.GOMAXPROCS(2)
    
    	var wg sync.WaitGroup
    
    	wg.Add(COUNT)
    
    	for i := 0; i < COUNT; i++ {
    		go func() {
    			a := 0
    
    			for i := 0; i < 1e7; i++ {
    				a++
    			}
    
    			wg.Done()
    
    		}()
    	}
    
    	wg.Wait()
    }
    

    我的电脑上有4个处理器,但是我在程序中设置了只用其中2个,以下是go scheduler做调度的trace记录。

    Lenovo@Lenovo-PC MINGW64 /f/hello
    $ GODEBUG=schedtrace=1000 go run main.go
    

    pic-011

    上述trace显示了全局队列中的goroutine数量,括号[xx yy]中为runqueue和本地队列(分别为P0和P1)。当本地队列满了,达到256个等待的goroutine时,接下来的goroutine将堆叠在全局队列中,我们可以看到runqueue属性在增长

    Goroutine并不是只有在本地队列满了的时候才会进入全局队列,当Go向调度器注入一个goroutine列表时(例如来自网络轮询器或垃圾收集过程中处于sleep状态的goroutine),它也会被推送进去。

    下面是前面例子的图。

    pic-2

    然而,您也许想知道为什么在前面的例子中,P0的本地队列不是空的。Go使用不同的策略来确保每个处理器保持忙碌。

    工作窃取

    当一个处理器没有任何工作(即G)时,它应用以下规则,直到可以有一个可以执行的G:

    • 从本地队列中抽取G
    • 从全局队列中抽取G
    • 从网络轮循器中抽调G
    • 从其它P的本地队列中获取G

    在我们前面的例子中,主函数正在P1上运行并创建goroutine,当第一个goroutine在P1的本地队列中添加后,P0正在寻找可以执行的G。然而,它的本地队列、全局队列和网络轮询器都是空的。所以它只能从P1的本地队列中去"窃取"可执行的G:

    pic-3

    下面的trace显示了在"窃取"前后的一个调度trace:

    pic-012

    trace显示了一个处理器如何从其他处理器那里窃取G的。它从本地队列中抽取了一半的goroutine;在7个goroutine中,有4个被窃取--其中一个立即在P0上运行,而其他三个则进入本地队列。现在各处理器之间的工作已经很平衡了。这一点可以通过执行跟踪得到证实:

    pic-4

    goroutine很好调度,由于没有I/O,所以goroutine是链式的,没有切换。现在我们来看看当涉及到一些I/O如文件操作时会发生什么。

    I/O和全局队列

    package main
    
    import (
    	"io/ioutil"
    	"strconv"
    	"sync"
    )
    
    func main() {
    	var wg sync.WaitGroup
    
    	for i := 0; i < 20; i++ {
    		wg.Add(1)
    		go func() {
    			a := 0
    
    			for i := 0; i < 1e6; i++ {
    				a += 1
    				if i == 1e6/2 {
    					bytes, _ := ioutil.ReadFile(`add.txt`)
    					inc, _ := strconv.Atoi(string(bytes))
    					a += inc
    				}
    			}
    
    			wg.Done()
    		}()
    	}
    
    	wg.Wait()
    }
    

    变量a现在会根据写入文件中的数字时时地递增。以下是新的trace:

    pic-5

    通过这个例子,我们可以看到每个goroutine并不是只由一个处理器处理的。在系统调用的情况下,当调用完成后,Go会使用网络轮询器,把全局队列中的goroutine拉回来。

    下面是对G35一个说明:

    pic-6

    由于一个处理器在没有任务时可以从全局队列中提取G,所以第一个可用的P将运行goroutine。这种行为解释了为什么一个goroutine会在不同的P上运行,并展示了Go如何优化系统调用与让其他goroutine在资源空闲时运行。

  • 相关阅读:
    结对开发第一阶段,10天冲刺第十天
    结对开发第一阶段,10天冲刺第九天
    vim 编辑器
    用户、群组和权限
    硬盘分区、格式化及文件系统管理
    系统监控
    Linux 系统初始化和服务
    文件合并与打包
    计算机原理
    Linux 之添加系统环境变量
  • 原文地址:https://www.cnblogs.com/double12gzh/p/13670964.html
Copyright © 2011-2022 走看看