Go 面试官:单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?

微信搜索【 脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blog 已收录,有个人系列文章、资料和开源 Go 图书。

你们好,我是煎鱼。git

最近金三银四,是面试的季节。在个人 Go 读者交流群里出现了许多小伙伴在讨论本身面试过程当中所遇到的一些 Go 面试题。github

今天的主角,是 Go 面试的万能题 GMP 模型的延伸题(疑问),那就是 ”GMP 模型,为何要有 P?“golang

进一步推敲问题的背后,其实这个面试题本质是想问:”GMP 模型,为何不是 G 和 M 直接绑定就完了,还要搞多一个 P 出来,那么麻烦,为的是什么,是要解决什么问题吗?“面试

这篇文章煎鱼就带你一同探索,GM、GMP 模型的变迁是由于什么缘由。算法

GM 模型

在 Go1.1 以前 Go 的调度模型其实就是 GM 模型,也就是没有 P。缓存

今天带你们一块儿回顾过去的设计。服务器

解密 Go1.0 源码

咱们了解一个东西的办法之一就是看源码,和煎鱼一块儿看看 Go1.0.1 的调度器源码的核心关键步骤:微信

static void
schedule(G *gp)
{
    ...
    schedlock();
    if(gp != nil) {
        ...
        switch(gp->status){
        case Grunnable:
        case Gdead:
            // Shouldn't have been running!
            runtime·throw("bad gp->status in sched");
        case Grunning:
            gp->status = Grunnable;
            gput(gp);
            break;
        }

    gp = nextgandunlock();
    gp->readyonstop = 0;
    gp->status = Grunning;
    m->curg = gp;
    gp->m = m;
    ...
    runtime·gogo(&gp->sched, 0);
}
  • 调用 schedlock 方法来获取全局锁。
  • 获取全局锁成功后,将当前 Goroutine 状态从 Running(正在被调度) 状态修改成 Runnable(能够被调度)状态。
  • 调用 gput 方法来保存当前 Goroutine 的运行状态等信息,以便于后续的使用;
  • 调用 nextgandunlock 方法来寻找下一个可运行 Goroutine,而且释放全局锁给其余调度使用。
  • 获取到下一个待运行的 Goroutine 后,将其的运行状态修改成 Running。
  • 调用 runtime·gogo 方法,将刚刚所获取到的下一个待执行的 Goroutine 运行起来。

思考 GM 模型

经过对 Go1.0.1 的调度器源码剖析,咱们能够发现一个比较有趣的点。那就是调度器自己(schedule 方法),在正常流程下,是不会返回的,也就是不会结束主流程。网络

G-M模型简图

他会不断地运行调度流程,GoroutineA 完成了,就开始寻找 GoroutineB,寻找到 B 了,就把已经完成的 A 的调度权交给 B,让 GoroutineB 开始被调度,也就是运行。并发

固然了,也有被正在阻塞(Blocked)的 G。假设 G 正在作一些系统、网络调用,那么就会致使 G 停滞。这时候 M(系统线程)就会被会从新放内核队列中,等待新的一轮唤醒。

GM 模型的缺点

这么表面的看起来,GM 模型彷佛牢不可破,毫完好陷。但为何要改呢?

在 2012 年时 Dmitry Vyukov 发表了文章《Scalable Go Scheduler Design Doc》,目前也依然成为各大研究 Go 调度器文章的主要对象,其在文章内讲述了总体的缘由和考虑,下述内容将引用该文章。

当前(代指 Go1.0 的 GM 模型)的 Goroutine 调度器限制了用 Go 编写的并发程序的可扩展性,尤为是高吞吐量服务器和并行计算程序。

实现有以下的问题:

  • 存在单一的全局 mutex(Sched.Lock)和集中状态管理:

    • mutex 须要保护全部与 goroutine 相关的操做(建立、完成、重排等),致使锁竞争严重。
  • Goroutine 传递的问题:

    • goroutine(G)交接(G.nextg):工做者线程(M's)之间会常常交接可运行的 goroutine。
    • 上述可能会致使延迟增长和额外的开销。每一个 M 必须可以执行任何可运行的 G,特别是刚刚建立 G 的 M。
  • 每一个 M 都须要作内存缓存(M.mcache):

    • 会致使资源消耗过大(每一个 mcache 能够吸纳到 2M 的内存缓存和其余缓存),数据局部性差。
  • 频繁的线程阻塞/解阻塞:

    • 在存在 syscalls 的状况下,线程常常被阻塞和解阻塞。这增长了不少额外的性能开销。

GMP 模型

为了解决 GM 模型的以上诸多问题,在 Go1.1 时,Dmitry Vyukov 在 GM 模型的基础上,新增了一个 P(Processor)组件。而且实现了 Work Stealing 算法来解决一些新产生的问题。

GMP 模型,在上一篇文章《Go 群友提问:Goroutine 数量控制在多少合适,会影响 GC 和调度?》中已经讲解过了。

以为不错的小伙伴能够关注一下,这里就再也不复述了。

带来什么改变

加了 P 以后会带来什么改变呢?咱们再更显式的讲一下。

  • 每一个 P 有本身的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减小。而 GM 模型的性能开销大头就是锁竞争。
  • 每一个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,若是 P 的本地队列为空,则会从全局队列或其余 P 的本地队列中窃取可运行的 G 来运行,减小空转,提升了资源利用率。

为何要有 P

这时候就有小伙伴会疑惑了,若是是想实现本地队列、Work Stealing 算法,那为何不直接在 M 上加呢,M 也照样能够实现相似的组件。为何又再加多一个 P 组件?

结合 M(系统线程) 的定位来看,若这么作,有如下问题:

  • 通常来说,M 的数量都会多于 P。像在 Go 中,M 的数量默认是 10000,P 的默认数量的 CPU 核数。另外因为 M 的属性,也就是若是存在系统阻塞调用,阻塞了 M,又不够用的状况下,M 会不断增长。
  • M 不断增长的话,若是本地队列挂载在 M 上,那就意味着本地队列也会随之增长。这显然是不合理的,由于本地队列的管理会变得复杂,且 Work Stealing 性能会大幅度降低。
  • M 被系统调用阻塞后,咱们是指望把他既有未执行的任务分配给其余继续运行的,而不是一阻塞就致使所有中止。

所以使用 M 是不合理的,那么引入新的组件 P,把本地队列关联到 P 上,就能很好的解决这个问题。

总结

今天这篇文章结合了整个 Go 语言调度器的一些历史状况、缘由分析以及解决方案说明。

”GMP 模型,为何要有 P“ 这个问题就像是一道系统设计了解,由于如今不少人为了应对面试,会硬背 GMP 模型,或者是泡面式过了一遍。而理解其中真正背后的缘由,才是咱们要去学的要去理解。

知其然知其因此然,才可破局。

如有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创做的最大动力,感谢支持。

文章持续更新,能够微信搜【脑子进煎鱼了】阅读,回复【 000】有我准备的一线大厂面试算法题解和资料;本文 GitHub github.com/eddycjy/blog 已收录,欢迎 Star 催更。
相关文章
相关标签/搜索