聊聊Goroutine-调度器数据结构(二)

161 篇文章 12 订阅

在《浅显的聊聊操作系统线程和线程调度》一文中聊过,内核对系统线程的调度可以简单归结为,在执行操作系统代码时,内核调度器按照一定的算法挑选出一个线程并将其在内存中的寄存器的值放入CPU对应的寄存器从而恢复该线程的执行。

系统线程对goroutine调度原理与内核对系统线程调度原理是一致的,本质上都是通过保存和修改CPU寄存器的值来达到切换系统线程或goroutine的目的。

也正是为了实现对goroutine的调度,所以引入了一种数据结构来保存CPU寄存器的值以及goroutine的其它状态信息,Go调度器源码中这种数据结构名为g。

g结构体保存了goroutine全部信息,它每一个实例对象都代表一个goroutine,调度器代码可以通过g对象来对goroutine进行调度,当goroutine被调离CPU时,调度器代码负责将CPU寄存器的值保存在g对象的成员变量中,当goroutine被调度起来运行时,调度器代码又负责将g对象的成员变量所保存的寄存器的值恢复到CPU的寄存器。

实现goroutine的调度仅有g结构体对象还是不够的,至少还需有一个存放所有可运行的goroutine的容器,便于工作线程寻找需要被调度起来运行的goroutine。

所以Go又引入了schedt结构体。

schedt是用来保存调度器自身状态信息,还有一个保存goroutine的运行队列。

因为每个Go程序只有一个调度器,所以各Go程序只有一个schedt结构体的实例对象,该对象在源码中被定义为共享的全局变量,如此每个工作线程都可以访问它以及其内的goroutine运行队列,可称此对列为全局运行队列。

既然有全局运行队列,那么对应的肯定会有局部的运行队列。原因很明显,全局运行队列每个工作线程都可以访问它,因此需要加锁,那在系统繁忙的时候,性能问题就不需要多说了。

所以调度器又为每个工作线程引入了一个私有的局部goroutine运行队列,工作线程优先使用自己的局部运行队列,只有必要时才会访问全局运行队列,如此大大减少锁冲突,提高了系统的并发性。

Go调度器源码中,局部运行队列被包含在p结构体实例对象中,每一个运行Go代码的工作线程都会与一个p结构体的实例对象关联在一起。

Go调度器源码中还有用来代表工作线程的m结构体,每个工作线程都有唯一一个m实例对象与其对应,m除了记录诸如【栈的起始位置】、【当前正在执行的goroutine】、【是否空闲】等状态信息之外,还通过指针维持着与p对象之间的绑定关系。

根据上述内容可推理出来,通过m既可以找到与之对应的工作现在正在运行的goroutine,又可以找到工作线程的局部运行队列等资源。

来看下上述几个结构体之间的关系图:

上图圆形代表g,三角形代表m,正方形代表p,其中红色g表示m对应的工作线程正在运行的goroutine,灰色g表示处于运行队列之中正等待被调度执行的goroutine。

可以看到,每个m绑定一个p,每个p都有一个私有的本地goroutine队列,m对应的线程从本地和全局goroutine中获取goroutine并将之运行。

再来看看工作线程与m是如何对应上的,其执行的代码又是如何找到属于自己的m?

如果只有一个工作线程,那么只会有一个m,那只需要定义全局m就可解决上述问题。

那多个工作线程和多个m又该如何对应?

这就要结合《最后看一下什么是线程本地存储(TLS)》一文聊过【线程本地存储其实就是线程私有的全局变量】这句话了。

只要每个工作线程拥有自己私有的m结构体的全局变量,那就可以在不同的工作线程中使用相同的全局变量名称来访问不同的m实例化对象。

结合goroutine调度器代码来说,每个工作线程在刚刚被创建出来进入调度循环之前,利用线程本地存储(TLS)机制为其实现一个指向m结构体的实例对象的私有全局变量,如此在之后代码中就使用该全局变量来访问自己的m以及与其关联的p、g。

再来看下上篇文章中调度伪代码完善一点之后的样子:

// 程序启动时的初始化代码......for i := 0; i < N; i++ { // 创建N个操作系统线程执行schedule函数     create_os_thread(schedule) // 创建一个操作系统线程执行schedule函数}// 定义一个线程私有全局变量,注意它是一个指向m结构体对象的指针// ThreadLocal用来定义线程私有全局变量ThreadLocal self *m //schedule函数实现调度逻辑func schedule() {    // 创建和初始化m结构体对象,并赋值给私有全局变量self    self = initm()       for { //调度循环          if (self.p.runqueue is empty) {                 // 根据某种算法从全局运行队列中找出一个需要运行的goroutine                 g := find_a_runnable_goroutine_from_global_runqueue()           } else {                 // 根据某种算法从私有的局部运行队列中找出一个需要运行的goroutine                 g := find_a_runnable_goroutine_from_local_runqueue()           }          run_g(g) // CPU运行该goroutine,直到需要调度其它goroutine才返回          save_status_of_g(g) // 保存goroutine的状态,主要是寄存器的值     }}

可以看到,不需要线程私有全局变量,仅在schedule中定义个局部变量就可以了。

真实调度代码错综复杂,不仅是schedule会需要访问m,还有很多地方需要访问它,所以要使用全局变量来方便其它对m以及与m相关联的p、g的访问。

至此,有关goroutine调度器数据结构就聊得差不多了,下篇文章详细看下Go调度代码中对m、p、g的定义。

以上仅为个人观点,不一定准确,能帮到各位那是最好的。

好啦,到这里本文就结束了,喜欢的话就来个三连击吧。

扫码关注公众号,获取更多优质内容。

  

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

luyaran

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值