当前位置: 首页 > news >正文

Go 面试系列: Goroutine 数量是越多越好吗?设置多少会影响GC调度呢?

Go 面试系列: Goroutine 数量是越多越好吗?设置多少会影响GC调度呢?

前言

现在的大厂都开始慢慢使用Go语言了,例如字节已经把Go作为后端开发的主要编程语言。但是Go的面试题总结的比较少,于是打算开启这个专栏,一起学习一起进步。


前几天被问到一个问题:“单机的 goroutine 数量控制在多少比较合适?”。

第一反应一样是答复 “控制多少,我觉得没有定论”。

紧接着延伸出了更进一步的疑惑:“goroutine 太多了会影响 gc 和调度吧,主要是怎么预算这个数是合理的呢?

这是本文要进行探讨的主体,因此本文的结构会是先探索基础知识,再一步步揭开,深入理解这个问题。

Goroutine 是什么

Go 语言作为一个新生编程语言,其令人喜爱的特性之一就是 goroutine。Goroutine 是一个由 Go 运行时管理的轻量级线程,一般称其为 “协程”。

go f(x, y, z)

操作系统本身是无法明确感知到 Goroutine 的存在的,Goroutine 的操作和切换归属于 “用户态” 中。

Goroutine 由特定的调度模式来控制,以 “多路复用” 的形式运行在操作系统为 Go 程序分配的几个系统线程上。

同时创建 Goroutine 的开销很小,初始只需要 2-4k 的栈空间。Goroutine 本身会根据实际使用情况进行自伸缩,非常轻量。

func say(s string) {
 for i := 0; i < 9999999; i++ {
  time.Sleep(100 * time.Millisecond)
  fmt.Println(s)
 }
}

func main() {
 go say("Viper")
 say("你好")
}

人称可以开几百几千万个的协程小霸王,是 Go 语言的得意之作之一。

调度是什么

既然有了用户态的代表 Goroutine,操作系统又看不到他。必然需要有某个东西去管理他,才能更好的运作起来。

这指的就是 Go 语言中的调度,最常见、面试最爱问的 GMP 模型。因此接下来将会给大家介绍一下 Go 调度的基础知识和流程。

下述内容摘自Viper和 p 神写的《Go 语言编程之旅》中的章节内容。

调度基础知识

Go scheduler 的主要功能是针对在处理器上运行的 OS 线程分发可运行的 Goroutine,而我们一提到调度器,就离不开三个经常被提到的缩写,分别是:

  • G:Goroutine,实际上我们每次调用 go func 就是生成了一个 G。
  • P:Processor,处理器,一般 P 的数量就是处理器的核数,可以通过 GOMAXPROCS 进行修改。
  • M:Machine,系统线程。

这三者交互实际来源于 Go 的 M: N 调度模型。也就是 M 必须与 P 进行绑定,然后不断地在 M 上循环寻找可运行的 G 来执行相应的任务。

调度流程

我们以 GMP 模型的工作流程图进行简单分析,官方图如下:

image-20210615222702234

  1. 当我们执行 go func() 时,实际上就是创建一个全新的 Goroutine,我们称它为 G。
  2. 新创建的 G 会被放入 P 的本地队列(Local Queue)或全局队列(Global Queue)中,准备下一步的动作。需要注意的一点,这里的 P 指的是创建 G 的 P。
  3. 唤醒或创建 M 以便执行 G。
  4. 不断地进行事件循环
  5. 寻找在可用状态下的 G 进行执行任务
  6. 清除后,重新进入事件循环

在描述中有提到全局和本地这两类队列,其实在功能上来讲都是用于存放正在等待运行的 G,但是不同点在于,本地队列有数量限制,不允许超过 256 个。

并且在新建 G 时,会优先选择 P 的本地队列,如果本地队列满了,则将 P 的本地队列的一半的 G 移动到全局队列。

这可以理解为调度资源的共享和再平衡。

窃取行为

我们可以看到图上有 steal 行为,这是用来做什么的呢,我们都知道当你创建新的 G 或者 G 变成可运行状态时,它会被推送加入到当前 P 的本地队列中。

其实当 P 执行 G 完毕后,它也会 “干活”,它会将其从本地队列中弹出 G,同时会检查当前本地队列是否为空,如果为空会随机的从其他 P 的本地队列中尝试窃取一半可运行的 G 到自己的名下。

官方图如下:

在这个例子中,P2 在本地队列中找不到可以运行的 G,它会执行 work-stealing 调度算法,随机选择其它的处理器 P1,并从 P1 的本地队列中窃取了三个 G 到它自己的本地队列中去。

至此,P1、P2 都拥有了可运行的 G,P1 多余的 G 也不会被浪费,调度资源将会更加平均的在多个处理器中流转。

有没有什么限制

在前面的内容中,我们针对 Go 的调度模型和 Goroutine 做了一个基本介绍和分享。

接下来我们回到主题,思考 “goroutine 太多了,会不会有什么影响”。

在了解 GMP 的基础知识后,我们要知道在协程的运行过程中,真正干活的 GPM 又分别被什么约束

Viper带大家分别从 GMP 来逐步分析。

M 的限制

第一,要知道在协程的执行中,真正干活的是 GPM 中的哪一个

那势必是 M(系统线程) 了,因为 G 是用户态上的东西,最终执行都是得映射,对应到 M 这一个系统线程上去运行。

那么 M 有没有限制呢?

答案是:有的。在 Go 语言中,M 的默认数量限制是 10000,如果超出则会报错:

GO: runtime: program exceeds 10000-thread limit

通常只有在 Goroutine 出现阻塞操作的情况下,才会遇到这种情况。这可能也预示着你的程序有问题。

若确切是需要那么多,还可以通过 debug.SetMaxThreads 方法进行设置。

G 的限制

第二,那 G 呢,Goroutine 的创建数量是否有限制?

答案是:没有。但理论上会受内存的影响,假设一个 Goroutine 创建需要 4k(via @GoWKH):

  • 4k * 80,000 = 320,000k ≈ 0.3G内存
  • 4k * 1,000,000 = 4,000,000k ≈ 4G内存

以此就可以相对计算出来一台单机在通俗情况下,所能够创建 Goroutine 的大概数量级别。

注:Goroutine 创建所需申请的 2-4k 是需要连续的内存块。

P 的限制

第三,那 P 呢,P 的数量是否有限制,受什么影响?

答案是:有限制。P 的数量受环境变量 GOMAXPROCS 的直接影响

环境变量 GOMAXPROCS 又是什么?在 Go 语言中,通过设置 GOMAXPROCS,用户可以调整调度中 P(Processor)的数量。

另一个重点在于,与 P 相关联的的 M(系统线程),是需要绑定 P 才能进行具体的任务执行的,因此 P 的多少会影响到 Go 程序的运行表现。

P 的数量基本是受本机的核数影响,没必要太过度纠结他。

那 P 的数量是否会影响 Goroutine 的数量创建呢?

答案是:不影响。且 Goroutine 多了少了,P 也该干嘛干嘛,不会带来灾难性问题。

何为之合理

在介绍完 GMP 各自的限制后,我们回到一个重点,就是 “Goroutine 数量怎么预算,才叫合理?”。

“合理” 这个词,是需要看具体场景来定义的,可结合上述对 GPM 的学习和了解。得出:

  • M:有限制,默认数量限制是 10000,可调整。
  • G:没限制,但受内存影响。
  • P:受本机的核数影响,可大可小,不影响 G 的数量创建。

Goroutine 数量在 MG 的可控限额以下,多个把个、几十个,少几个其实没有什么影响,就可以称其为 “合理”。

真实情况

在真实的应用场景中,没法如此简单的定义。如果你 Goroutine:

  • 在频繁请求 HTTP,MySQL,打开文件等,那假设短时间内有几十万个协程在跑,那肯定就不大合理了(可能会导致 too many files open)。
  • 常见的 Goroutine 泄露所导致的 CPU、Memory 上涨等,还是得看你的 Goroutine 里具体在跑什么东西。

还是得看 Goroutine 里面跑的是什么东西。

总结

在这篇文章中,分别介绍了 Goroutine、GMP、调度模型的基本知识,针对如下问题进行了展开:

  • 单机的 goroutine 数量控制在多少比较合适?
  • goroutine 太多了会影响 gc 和调度吧,主要是怎么预算这个数是合理的呢?

单机的 goroutine 数量只要控制在限额以下的,都可以认为是 “合理”。

真实场景得看具体里面跑的是什么,跑的如果是 “资源怪兽”,只运行几个 Goroutine 都可以跑死。

如下问题进行了展开:

  • 单机的 goroutine 数量控制在多少比较合适?
  • goroutine 太多了会影响 gc 和调度吧,主要是怎么预算这个数是合理的呢?

单机的 goroutine 数量只要控制在限额以下的,都可以认为是 “合理”。

真实场景得看具体里面跑的是什么,跑的如果是 “资源怪兽”,只运行几个 Goroutine 都可以跑死。

因此想定义 “预算”,就得看跑的什么了。

相关文章:

  • 什么是读、写扩散?
  • 一文搞定权限设计模型(RBAC,ABAC)超详细图文解析
  • 一文搞定权限管理!授权、鉴权超详细解析
  • Go 中的 JSON如何序列化和反序列化?来看看go的包怎么实现!
  • Go中如何比较两个json?深度优先搜索解决,超详细代码!
  • Go语言实现枚举方法,const和iota结合轻松实现
  • Go msgp序列化使用详解!比Json更快!面试时吊打面试官!
  • 缓存击穿了怎么办?使用singleflight轻松解决!
  • Go中优雅的获取Map元素的多种方法
  • Go中的nil是是什么?和java的null有区别吗?
  • 大厂面试必会语言:GO语言入门,看这一篇就够了
  • 无需安装!Windows11网页版来了!一键带你体验win11!
  • Go语言里如何采用面向对象编程?Go中一样能够面向对象!
  • Go 面试系列:如何比较GO中的结构体?
  • Go面试系列:Goroutine为什么设计为没有ID?
  • JavaScript的使用你知道几种?(上)
  • js ES6 求数组的交集,并集,还有差集
  • Js基础知识(一) - 变量
  • js继承的实现方法
  • PermissionScope Swift4 兼容问题
  • PHP 7 修改了什么呢 -- 2
  • Redux系列x:源码分析
  • ucore操作系统实验笔记 - 重新理解中断
  • Vue2.0 实现互斥
  • vue的全局变量和全局拦截请求器
  • 包装类对象
  • 互联网大裁员:Java程序员失工作,焉知不能进ali?
  • 再谈express与koa的对比
  • 在GitHub多个账号上使用不同的SSH的配置方法
  • 中文输入法与React文本输入框的问题与解决方案
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • ​Java并发新构件之Exchanger
  • ​ssh免密码登录设置及问题总结
  • #Lua:Lua调用C++生成的DLL库
  • #NOIP 2014# day.1 T2 联合权值
  • $L^p$ 调和函数恒为零
  • (二)基于wpr_simulation 的Ros机器人运动控制,gazebo仿真
  • (黑马C++)L06 重载与继承
  • (三)Hyperledger Fabric 1.1安装部署-chaincode测试
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • .gitignore文件_Git:.gitignore
  • .NET delegate 委托 、 Event 事件
  • .Net Remoting(分离服务程序实现) - Part.3
  • .NET 中各种混淆(Obfuscation)的含义、原理、实际效果和不同级别的差异(使用 SmartAssembly)
  • @Conditional注解详解
  • @selector(..)警告提示
  • @在php中起什么作用?
  • [20170713] 无法访问SQL Server
  • [8-23]知识梳理:文件系统、Bash基础特性、目录管理、文件管理、文本查看编辑处理...
  • [Android]Tool-Systrace
  • [BZOJ] 2006: [NOI2010]超级钢琴
  • [BZOJ5250][九省联考2018]秘密袭击(DP)
  • [C#]手把手教你打造Socket的TCP通讯连接(一)
  • [C语言][PTA基础C基础题目集] strtok 函数的理解与应用
  • [C语言]编译和链接