GMP
线程
内核态,操作系统的最小调度单元,创建销毁,调度都需要由内核来完成,可以充分利用多核,实现并行
协程
- 用户态,是线程的子集,在线程的基础上,对线程进行二次加工得到,从属于某一个线程,与线程存在映射关系比例是m : 1。
- 某一个协程陷入阻塞的话,这个阻塞会上升到整个线程
对操作系统而言,协程对它是透明的,它只能看到线程阻塞了
协程没法做到真正意义上的并行,只是在用户视角下的并发
goruntine
-
golang对协程的优化,在goruntine和线程之间加了一个中间层,调度器,让协程不在依附于线程,实现动态组合关联,与线程的映射关系为M:N
-
继承了协程的优点,创建、销毁、调度都由用户态来完成,无需内核介入,更轻量
-
可以做到真正意义上的并行
-
某一个陷入阻塞的时候,会被调度器感知到,调度器会实现goruntine与线程的解绑释放资源
为什么协程无法做到真正意义上的并行,而goruntine可以做到 -
普通的协程是N:1关系,操作系统只看得到这一个线程,开了1000个协程,也只分配一个cpu核心的时间片,同一时刻只有一个协程在运行,属于并发但不是并行
-
go程序在启动的时候,会根据cpu核心数创建多个 P (Processor,逻辑处理器)。
每个 P 都会绑定一个系统线程 M (Machine)。
关键点:如果有 8 个 CPU 核心,Go 就会创建 8 个系统线程同时工作。调度器会把成千上万个 Goroutine 分发给这 8 个线程。
结果:物理上确实有 8 个 Goroutine 在同一纳秒内同时运行
GMP
G:goruntine M:machine(内核视角下的线程) P:processor
对g而言,p相当于它的cpu,对m而言,p相当于它的执行代理,m要和p结合之后才能执行调度goruntine
对于每一个p而言,它会先尝试从自己的本地队列获取gorunine,每个p私有队列的话,可以设计到更少的并发,接近于无锁化,如果私有队列没有了,就去全局队列获取。如果本地队列以及全局队列都没有的话,就会尝试从其他的P的私有队列窃取,这涉及到并发问题,所以还是要加锁,所以说是接近无锁化
核心数据结构
G
1 | type g struct { |
m0
m0是Go程序创建的第一个系统线程,
- 唯一性:在整个 Go 进程的生命周期中,
m0是全局唯一的(它是定义在全局变量中的)。 - 静态分配:普通的 M 是在需要时动态创建(malloc)的,而
m0是在程序启动之初就定义好的全局变量,不需要动态分配内存
作用: - 负责Go程序的启动流程,调度器初始化,内存管理器初始化,垃圾回收器等,负责启动第一个goruntine执行main.main函数,之后参与正常的调度循环,与其他的m没区别。
- 在程序退出时还负责处理清理工作,比如等待其他goroutine结束、执行defer函数等
goruntine的生命周期
1 | __ _Gsyscall __ |
_ Gsyscall: 系统调用
_ Gwaiting:等待中,G被阻塞了(channel读写, select,mutex),此时的g不在队列中,必须被唤醒才能到grunnable
1 | type m struct { |
g0
m0是全局唯一的,但是g0不是,每一个M(线程)都有一个属于自己的 g0。
g0不执行用户的代码,负责执行调度逻辑,包括决定下一个运行哪个g,创建新g,
g0有较大的栈8MB,普通的g只有2kb,因此,当g要进行以下操作时,必须先切换到g0:
寻找下一个g,调用newproc创建新的g,用户栈溢出时进行扩容,垃圾回收
g0与g的转换:g0 -> g: gogo() ,
g -> g0 :mcall(),systemstack()
调度类型
主动调度
runtime.Gosched():用户显式调用,G 的状态从 _Grunning 变为 _Grunnable,然后被放入全局队列。注意,主动放弃的 G 通常不放回本地队列,而是放全局,避免它立刻又被拿出来运行
被动调度
当 G 执行某些阻塞操作时,运行时会调用 gopark 把当前 G 的状态从 _Grunning 改为 _Gwaiting,并将其从 M 上移除。M 此时变为空闲,可以去执行队列里的下一个 G
正常调度
goruntine的代码逻辑正常执行完了,最后进行正常的销毁,调用m_call把执行权给到g0
抢占调度
某一个goruntine执行了太久时间,被全局监控者monitor g监控到,被强制和p解绑
宏观调度视角

schedule():调用findRunnable(),找到下一个要被执行的goruntine
1 | func schedule() { |
findRunnable找m的顺序
- 插队的,看看P的runnext字段有没有G,(每经历61次调度之后,需要优先处理一次全局队列防止饥饿)
- 看看P的runq本地队列里面有没有g,(CAS无锁,访问比较快)
- 去全局队列里面找,需要加锁
- 检查网络轮询器,看看有没有 G 是因为网络 I/O 阻塞的,现在已经准备好了(Ready)。如果有,把它变成
_Grunnable拿过来 - 本地/全局/网络都没有,就去窃取别的P的本地私有队列,偷一半
- 还是没有找到,进入休眠状态,需要唤醒


