一文搞懂Go 的并发调度模型

线程模型

在细说 Go 的调度模型之前,先来说说一般意义的线程模型。线程模型一般分三种,由用户级线程和 OS 线程的不同对应关系决定的。

N:1,即全部用户线程都映射到一个OS线程上,上下文切换成本最低,但无法利用多核资源;

**1:1 **, 一个用户线程对应到一个 OS线程上, 能利用到多核资源,但是上下文切换成本较高,这也是 Java Hotspot VM 的默认实现;

M:N,权衡上面两者方案,既能利用多核资源也能尽可能减少上下文切换成本,但是调度算法的实现成本偏高。

为什么 Go Scheduler 需要实现 M:N 的方案?

线程创建开销大。对于 OS 线程而言,其很多特性均是操作系统给予的,但对于 Go 程序而言,其中很多特性可能非必要的。这样一来,如果是 1:1 的方案,那么每次** go func(){...} **都需要创建一个 OS 线程,而在创建线程过程中,OS 线程里某些 Go 用不上的特性会转化为不必要的性能开销,不经济。

减少 Go 垃圾回收的复杂度。依据1:1方案,Go 产生所用用户级线程均交由 OS 直接调度。 Go 的垃圾回收器要求在运行时需要停止所有线程,才能使得内存达到稳定一致的状态,而 OS 不可能清楚这些,垃圾回收器也不能控制 OS 去阻塞线程。

Go Scheduler 的 M:N 方案出现,就是为了解决上面的问题。

Go Scheduler

image

整个并发模型的讨论都离不开 Go Scheduler 的设计实现。

首先,需要了解的是,Go Scheduler 由哪些元素构成呢?

M: Machine,就是 OS 线程本身,数量可配置;

P: Processor, 调度器的核心处理器,通常表示执行上下文,用于匹配 M 和 G 。P 的数量不能超过 **GOMAXPROCS **配置数量,这个参数的默认值为CPU核心数;通常一个 P 可以与多个 M 对应,但同一时刻,这个 P 只能和其中一个 M 发生绑定关系;M 被创建之后需要自行在 P 的 free list 中找到 P 进行绑定,没有绑定 P 的 M,会进入阻塞态。

注:GOMAXPROCS 参数很重要,其决定了 P 的最大数量,也决定了自旋 M 的最大数量。何为自旋,后面会提到。

G: Goroutine,Go 的用户级线程,常说的协程,真正携带代码执行逻辑的部分,由 **go func(){...} **直接生成;

**G0: **其本身也是 G ,也需要跟具体的 M 结合才能被执行,只不过他比较特殊,其本身就是一个 schedule 函数,这个函数包含如下逻辑:

/src/runtime/proc.go:

func schedule() {

这里涉及到另外几个概念,本地队列、全局队列以及 “窃取”。

本地队列(local queue): 本地是相对 P 而言的本地,每个 P 维护一个本地队列;与 P 绑定的 M 中如若生成新的 G,一般情况下会放到 P 的本地队列;当本地队列满了的时候,才会截取本地队列中 “一半” 的元素放入全局队列中;

全局队列(global queue):承载本地队列“溢出”的 G。为了保证调度公平性,schedule 过程中有 1/61 的几率优先检查全局队列,否则本地队列一直满载的情况下,全局队列中的 G 将永远无法被调度到;

窃取(stealing): 这似乎和 Java Fork-Join 中的 work-stealing 模型很相似,其目的也是一样,就是为了使得空闲(idle)的 M 有活干,不空等,提高计算资源的利用率。窃取也是有章法的,规则是随机从其他 P 的本地队列里窃取 “一半” 的 G。

image

综上,整个调度流程就是:

  • 1/61 的几率在全局队列中找 G,60/61 的几率在本地队列找 G;

  • 如果全局队列找不到 G,从 P 的本地队列找 G;

  • 如果找不到,从其他 P 的本地队列中窃取 G;

  • 如果找不到,则从全局队列中拿取一部分 G 到本地队列。这里拿取的 “一部分” 满足一个公式:

    n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))

    注:这里 GQ 表示全局队列。

  • 如果找不到,从网络中 poll G。

只要找到了 G, 就会立马丢给 M 执行。当然上述任何执行逻辑如果没有 running 的 M 参与,都是无法真正被执行的,这包括调度逻辑本身。

一言蔽之,调度的本质就是 P 将 G 合理的分配给某个 M 的过程。

线程自旋(Spinning Threads)

线程自旋是相对于线程阻塞而言的,表象就是循环执行一个指定逻辑(就是上面提到的调度逻辑,目的是不停地寻找 G)。这样做的问题显而易见,如果 G 迟迟不来,CPU 会白白浪费在这无意义的计算上。但好处也很明显,降低了 M 的上下文切换成本,提高了性能。

具体来说,假设Scheduler 中全局和本地队列均为空,M 此时没有任何任务可以处理,那么你会选择让 M 进入阻塞状态还是选择让 CPU 空转等待 G 的驾临?

Go 的设计者倾向于高性能的并发表现,选择了后者。当然前面也提到过,为了避免过多浪费 CPU 资源,自旋的线程数不会超过 **GOMAXPROCS **,这是因为一个 P 在同一个时刻只能绑定一个 M,P的数量不会超过 GOMAXPROCS,自然被绑定的 M 的数量也不会超过。对于未被绑定的“游离态”的 M,会进入休眠阻塞态。

M 如果因为 G 发起了系统调用进入了阻塞态会怎样?

image

如图,如果 G8 发起了阻塞系统调用(例如阻塞 IO 操作),使得对应的 M2 进入了阻塞态。此时如果没有任何的处理,Go Scheduler 就会在这段阻塞的时间内,白白缺失了一个 OS 线程单元。

Go 设计者的解决方案是,一旦 G8 发起 Syscall 使得 M2 进入阻塞态,此时的 P2 会立即与 M2 解绑,保留 M2 与 G8 的关系,继而与新的 OS 线程 M5 绑定,继续下一轮的调度。那么虽然 M2 进入了阻塞态,但宏观来看,并没有缺失任何处理单元,P2 依然正常工作。

那 G8 的阻塞操作返回后怎么办?

G8 失去了 P2,意味着失去了执行机会,M2 被唤醒以后第一件事就是要窃取一个上下文(Processor),还给 G8 执行机会。然而现实是 M2 不一定能够找到 P 绑定,不过找不到也没关系,M2 会将 G8 丢到全局队列中,等待调度。

这样一来 G8 会被其他的 M 调度到,重新获得执行机会,继续执行阻塞返回之后的逻辑。

与 Java F-J Task 相比,Goroutine 是否有本质区别?

直觉告诉我,这两者并没有本质区别。

两个方案均为 M:N,两者定位都是“轻量级”的用户线程,而并非与 OS 线程一一对应。引入的本地队列使得在大多数场景下,线程争抢会更少。而从调度算法细分析来看,Go Scheduler 会比 F-J 更加复杂,对线程的控制更加精细,其适用的场景也更加全面。

总之,作为一个 Java 开发者,如果你是用 Hotspot VM,是默认不支持原生协程的,但不代表不能实现所谓的协程,F-J 可以理解为一个简易版实现。

然而 Go 并发调度的优势在哪呢?

原生支持;调度算法更加科学,更具备普适性;另外,简洁的语法掩盖了底层细节,这让开发者轻松实现高性能并发编程,成为可能。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容