goroutine调度器的Q&A


goroutine调度器的 Q&A

go栈和用户栈如何切换

goroutine如何退出

  1. main goroutine执行完毕后整个进程退出,其它子goroutine也就结束了

goroutine调度时机有哪些

goroutine和线程的区别

从三个角度比较goroutine和tread的区别:内存消耗、创建于销毁、切换

  1. 内存消耗:
    创建一个goroutine的栈内存消耗为2KB, 实际运行过程中,如果栈空间不够,会自动进行扩容,
    创建一个thread则需要消耗1MB栈内存,对于一个用go构建的http server来说,对于每个到来的请求
    创建一个goroutine去处理是件非常简单的事情,但是在python中,如果为每个请求创建一个线程去处理
    太浪费资源了,并发一高就会包OOM错误,OutOfMermoryError
  2. 创建于销毁
    Thread创建于销毁都有比较大的消耗,因为要和操作系统打交道,是内核级的,通常使用线程池来解决
    goroutine是由Go Runtime进行管理的,创建和销毁的消耗非常小,是用户级的
  3. 切换
    thread切换时需要保存各种寄存器,以便将来恢复,thread切换大约需要消耗1000-1500纳秒
    goroutine切换时只需要保存三个寄存器即可,goroutine切换大约需要消耗200纳秒
    goroutine切换要比thread切换成本小很多

GPM是什么

  1. G、P、M是go调度器的三个核心组件,各司其职,在它们的精密配合下,go调度器得以高效运转,
    这也是go天然支持高并发的内在动力,我们了解一下GPM模型
  2. G: 取goroutine的首字母,主要保存goroutine的一些状态信息和CPU的一些寄存器的值,例如IP寄存器,
    以便轮到本goroutine执行时,CPU知道从哪一条指令开始执行
  • 当goroutine被调离CPU时,调度器负责把CPU寄存器的值保存到g对象的成员变量中
  • 当goroutine被调度起来运行时,调度器又负责把g对象成员变量所保存的寄存器的值加载到CPU寄存器上
  1. M: 取machine的首字母,它代表一个工作线程或者说是系统线程,G需要调度到M上才能运行,M是真正工作的人
    结构体m就是我们常说的M, 它保存了M自身使用的栈信息,当前正在M上执行的G信息,与之绑定的P信息
    当M没有工作可做的时候,在它休眠前,会"自旋"的来找工作,检查全局队列---查看network pooler---视图执行gc任务---或者偷工作
  2. P: processor的首字母,为M的执行提供上下文,保存M执行G时的一些资源,例如本地可运行G队列,memory cache
    一个M只有绑定了P才能执行goroutine, 当M被阻塞时,整个P会传递给其它M,或者说整个P被接管
  3. GPM三足鼎立,共同成就了go scheduler, G需要在M上才能运行,M需要P提供的资源,P本地队列存储待运行的G
    你中有我,我中有你
  4. M会从它绑定的P的本地队列获取待运行的G,也会从network pooler中获取待运行的G,也会从全局队列获取g, 也会从其它p队列中偷取g运行
  5. M只有自旋和非自旋两种状态,自旋的时候,会努力找工作,找不到的时候会进入非自旋状态,之后会休眠,
    知道有工作需要处理时,被其它工作线程唤醒,有进入自旋状态

M如何找工作

  1. 在schedule函数中,我们看一下如何查找一个runable goroutine的过程,
    工作线程M要费劲心机的找到一个可运行的goroutine, 因为这是它的职责。
    经历三个步骤:线程M先从本地P队列里了找,定期从全局队列里找,如果没有就从其它P队列里偷
  2. go调度器并不是每次都会从全局队列获取可运行的G, 实际情况是调度器没调度61次并且全局队列有可运行的G时
    才会调用globrunqget函数从全局队列获取goroutine, 毕竟从全局队列获取需要上锁,性能开销就大了
  3. runqget globrunqget findrunable
  4. M首先获取当前指向的g,也就是g0,然后到其绑定的p

什么是go scheduler

  1. go程序的执行由两部分组成,Go Program, Runtime, 即用户程序和运行时,它们之间通过函数调用来实现
    内存管理,channel通信,goroutine创建等功能,用户程序进行的系统调用都会被Runtime拦截,
    以此来帮助调度或垃圾回收相关的工作
  2. 为什么要scheduler
    go scheduler可以说是go运行时的最重要的一个部分了,Runtime维护所有的goroutines,并由scheduler进行调度
    goroutines和threads是独立的,但是goroutine要依赖于thread才能执行
    Go程序执行的高效和scheduler调度器是分不开的
func main() {
    // NumCPU 返回当前进程可以用到的逻辑核心数
    fmt.Println(runtime.NumCPU())
}

NumCPU返回的是逻辑核心数,而非物理核心数
3. go程序启动后,会给每个逻辑核心分配一个p, 同时会给每个p分配一个M(内核线程), M由os scheduler来调度
总结一下,当我本地启动一个go程序的时候,会得到4个系统线程去执行任务,每个线程搭配一个P
4. go scheduler是go runtime的一部分,它内嵌在go程序里,和go程序一起运行,因此它运行在用户空间,是kernel的上一层
和os scheduler抢占式调度不一样,go scheduler采用协作式调度
协作式调度一般会由用户设置调度点,像python中通过yield告诉os scheduler可以江都调度出去了
5. 但是在go语言里,goroutine的调度是由go runtime来做的,并非由用户控制,所以我们依然可以将go scheduler看做是
抢占式调度,因为我们也不知道go scheduler下一步做什么,和线程类似,goroutine的状态也是3种
Waiting等待状态,Runnable就绪状态, Executing运行状态

什么是M:N模型

  1. go runtime负责goroutine的生老病死,从创建到销毁,一手接管,Runtime会在程序启动的时候创建M个线程,
    之后创建的N个goroutine都会依附在这M个线程上执行,这就是M:N模型
    在同一时刻,一个M线程只能跑一个G goroutine, 当goroutine发生阻塞时,runtime会把当前阻塞的goroutine调度走,
    由其它goroutine执行,目的就是不让一个M线程闲着,榨干CPU的每一滴油水

什么是workstealing机制

  1. 当M绑定的P的本地队列没有可运行的G时,就会去其它P队列偷一半G回来继续运行

描述scheduler的初始化过程

  1. go scheduler在源码中的结构体为:schedt, 保存调度器的状态信息,全局的可运行G队列
  2. 不仅是go程序,系统加载可执行文件都会经历以下几个步骤
  • 从磁盘读取可执行文件,加载到内存
  • 创建进程和主线程
  • 为主线程分配栈空间
  • 把由用户在命令行输入的参数拷贝到主线程的栈
  • 把主线程放到操作系统的运行队列等待被调度

相关