Go语言调度模型G、M、P的数量多少合适?

百度一下Go语言优势,几乎所有文章都包含并发性好,作为一名老PHPer,一番学习实践下来,真香。

在当今这个多核时代,并发编程的意义不言而喻。当然,很多语言都支持多线程、多进程编程,但遗憾的是,实现和控制起来并不是那么令人感觉轻松和愉悦。Golang不同的是,语言级别支持协程(goroutine)并发(协程又称微线程,比线程更轻量、开销更小,性能更高),操作起来非常简单,语言级别提供关键字(go)用于启动协程,并且在同一台机器上可以启动成千上万个协程。

不管作为初学者还是久经沙场的老Gopher,Golang的协程调度器原理及 GMP 设计思想都是有必要去掌握的,而且面试必问:),推荐阅读丹冰大佬的 Golang修养之路GMP章节,图文并茂非常Nice。想深入学习的推荐欧神的 并发调度,结合源码和流程图讲解,膜拜。本文结束?Too young too simple,真正的重头戏才刚刚开始。

G 的数量:

无限制,理论上受内存的影响,创建一个 G 的初始栈大小为2-4K,配置一般的机器也能简简单单开启数十万个 Goroutine ,而且Go语言在 G 退出的时候还会把 G 清理之后放到 P 本地或者全局的闲置列表 gFree 中以便复用。

那是不是为了提高“并发能力”,就可以为所欲为的开启 Goroutine 呢,答案是否定的。

如果 Goroutine 中只有简单的逻辑,比如输出Hello world:),那肯定是没什么问题,但是如果Goroutine 中存在频繁请求 HTTP,MySQL,打开文件等,那假设短时间内有几十万个协程在跑,那肯定就不大合理了(可能会导致 too many files open)。常见的 Goroutine 泄露所导致的 CPU、Memory 上涨等,所以还是得看你的 Goroutine 里具体在跑什么东西。

一开始写 GO ,不管多大数据处理全部丢进去进行循环,认为全部都并发使用 Goroutine 去做一件事情,效率比较高,但这样的话,噩梦般的事情就开始了,服务器系统资源利用率不断上涨,到最后程序自动killed。这里比较好的解决方案是,引入线程池,限制 Goroutine 的数量,复用资源,保障系统稳定的同时提高处理能力。避免重复造轮子,推荐使用 ants,源码也不多可以学习学习,亲测好用- -

M 的数量:

限制10000,Go语言运行时初始化的时候在runtime.schedinit()中通过下面代码设置了 M 的最大数。

sched.maxmcount = 10000

通过debug.SetMaxThreads(n),可以调整。如果超出(手动调成10)会panic:

runtime: program exceeds 10-thread limit
fatal error: thread exhaustion

因为 M 必须持有 P 才能运行 G,通常情况 P 的数量很有限,如果 M 还超过 10000,基本上就是程序写的有问题。

P 的数量:

有限制,默认是CPU核心数,由启动时环境变量$GOMAXPROCS或者是由runtime.GOMAXPROCS()决定,runtime初始化建议阅读煎鱼大佬的 详解 Go 程序的启动流程,你知道 g0,m0 是什么吗?,以及 Go语言调度器之创建main goroutine

func schedinit() {
     ...
     procs := ncpu    // osinit 中获取 CPU 核心数
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    // P 初始化
    if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
    }
     ...
}
func GOMAXPROCS(n int) int {
    ...

    stopTheWorldGC("GOMAXPROCS")

    // newprocs will be processed by startTheWorld
    newprocs = int32(n)      // 重新设置的 P 数量

    startTheWorldGC()
    return ret
}

startTheWorldGC() -> startTheWorld() -> startTheWorldWithSema()

func startTheWorldWithSema(emitTraceEvent bool) int64 {
    ...
    procs := gomaxprocs
    if newprocs != 0 {
        procs = newprocs
        newprocs = 0
    }
    // 扩容或者缩容全局的处理器
    p1 := procresize(procs)
    ...
}

在任何情况下,Go运行时并行执行(注意,不是并发)的 goroutines 数量是小于等于 P 的数量的。为了提高系统的性能,P 的数量肯定不是越小越好,所以官方默认值就是 CPU 的核心数,设置的过小的话,如果一个持有 P 的 M,由于 P 当前执行的 G 调用了 syscall 而导致 M 被阻塞,那么此时关键点:GO 的调度器是迟钝的,它很可能什么都没做,直到 M 阻塞了相当长时间以后,才会发现有一个 P/M 被 syscall 阻塞了。然后,才会用空闲的 M 来强这个 P。通过 sysmon 监控实现的抢占式调度,最快在20us,最慢在10-20ms才会发现有一个 M 持有 P 并阻塞了。操作系统在 1ms 内可以完成很多次线程调度(一般情况1ms可以完成几十次线程调度),Go 发起 IO/syscall 的时候执行该 G 的 M 会阻塞然后被OS调度走,P什么也不干,sysmon 最慢要10-20ms才能发现这个阻塞,说不定那时候阻塞已经结束了,宝贵的P资源就这么被阻塞的M浪费了。
补充说明:调度器迟钝不是 M 迟钝,M 也就是操作系统线程,是非常的敏感的,只要阻塞就会被操作系统调度(除了极少数自旋的情况)。但是 GO 的调度器会等待一个时间间隔才会行动,这也是为了减少调度器干预的次数。也就是说,如果一个 M 调用了什么 API 导致了操作系统线程阻塞了,操作系统立刻会把这个线程M调度走,挂起等阻塞解除。这时候,Go 调度器不会马上把这个 M 持有的 P 抢走。这就会导致一定的 P 被浪费了。
开源数据库项目https://github.com/dgraph-io/dgraph中,特意将 GOMAXPROCS 调整到 128 增加 IO 处理能力,提高吞吐量。

https://github.com/dgraph-io/dgraph/blob/master/dgraph/main.go

func main() {
    rand.Seed(time.Now().UnixNano())
    // Setting a higher number here allows more disk I/O calls to be scheduled, hence considerably
    // improving throughput. The extra CPU overhead is almost negligible in comparison. The
    // benchmark notes are located in badger-bench/randread.
    runtime.GOMAXPROCS(128)
}

那 P 的数量太大会有什么影响呢?
一个runtime findrunnable 时产生的损耗,另一个是线程引起的上下文切换。如果是cpu密集的业务,增加多个processor也没用,毕竟cpu计算资源就这些,来回切换反而拖慢程序。

runtime的 findrunnable 方法是解决 M 找可用的协程的函数,当从绑定 P 本地runq上找不到可执行的goroutine后,尝试从全局链表中拿,再拿不到从 netpoll 和事件池里拿,最后会从别的 P 里偷任务。全局 runq 是有锁操作,其他偷任务使用了atomic 原子操作来规避futex竞争下陷入切换等待问题,但 lock free 在竞争下也会有忙轮询的状态,比如不断的尝试(自旋)。

随着调多 runtime processor 数量,相关的 M 线程自然也就跟着多了起来。linux 内核为了保证可执行的线程在调度上雨露均沾,按照内核调度算法来切换就绪状态的线程,切换又引起上下文切换。上下文切换也是性能的一大杀手。findrunnable 的某些锁竞争也会触发上下文切换。

结论:常规项目直接使用默认的核心数就好了,GOMAXPROCS 开太多的时候,针对计算密集型的处理性能提升反而没那么大,IO 密集(或者 syscall 较多)的 Go 程序,至少应该配置到CPU核心数目的5倍以上, 最大1024。

个人学习笔记,方便自己复习,有不对的地方欢迎评论哈!

参考资料:

Golang修养之路GMP章节
并发调度
详解 Go 程序的启动流程,你知道 g0,m0 是什么吗?
Go语言调度器之创建main goroutine
Go 群友提问:Goroutine 数量控制在多少合适,会影响 GC 和调度?
Go开发中,如何有效控制Goroutine的并发数量
golang gomaxprocs调高引起调度性能损耗
[GO语言]合理配置GOMAXPROCS提升一倍以上的性能

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

推荐阅读更多精彩内容