Go为什么能这么“快”?
极客时间编辑部
讲述:丁婵大小:8.83M时长:06:26
来源:腾讯技术工程(ID:Tencent_TEG)
Go 从 2009 年正式发布以来,依靠其极高运行速度和高效的开发效率,迅速占据市场份额。它为什么能这么“快”呢?近日,腾讯 CSIG 后台开发工程师 joellwang 介绍了 Go 程序为了实现极高的并发性能,其内部调度器的实现架构(G-P-M 模型),以及为了最大限度利用计算资源,Go 调度器是如何处理线程阻塞的场景。以下为重点内容。
Golang 从语言级别支持并发,通过轻量级协程 Goroutine 来实现程序并发运行。
Goroutine 非常轻量,主要体现在以下两个方面:
上下文切换代价小: Goroutine 上下文切换只涉及到三个寄存器(PC / SP / DX)的值修改。而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器、PC、SP 等寄存器的刷新。
内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K。Golang 程序中可以轻松支持 10W 级别的 Goroutine 运行,而线程数量达到 1K 时,内存占用就已经达到 2G。
Go 调度器实现机制
Go 程序通过调度器来调度 Goroutine 在内核线程上执行,但是 Goroutine 并不直接绑定 OS 线程 M 运行,而是由 Goroutine Scheduler 中的 P 来作获取内核线程资源的“中介”。
Go 调度器模型通常被称为 G-P-M 模型,它包括 4 个重要结构,分别是 G、P、M、Sched。
G(Goroutine):每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。
P(Processor):表示逻辑处理器。P 的数量决定了系统内最大可并行的 G 的数量,前提是物理 CPU 核数≥P 的数量。
M(Machine): OS 内核线程抽象,代表真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环。而 schedule 循环的机制大致是从 Global 队列、P 的本地队列以及 wait 队列中获取。M 的数量是不定的,由 Go Runtime 调整,M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。
Sched: Go 调度器。它维护存储 M 和 G 的队列以及调度器的一些状态信息。调度器循环的机制大致是从各种队列、P 的本地队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 Goexit 做清理工作并回到 M,如此反复。
在 Go 程序中,通过下面的图示来展示 G-P-M 模型:
P 代表可以“并行”运行的逻辑处理器,每个 P 都被分配到一个系统线程 M,G 代表 Go 协程。
Go 调度器中有两个不同的运行队列:全局运行队列 (GRQ) 和本地运行队列 (LRQ)。
每个 P 都有一个 LRQ,用于管理分配在 P 的上下文中执行的 Goroutines,这些 Goroutine 轮流被与 P 绑定的 M 进行上下文切换。GRQ 适用于尚未分配给 P 的 Goroutines。
G 的数量可以远远大于 M 的数量,换句话说,Go 程序可以利用少量的内核级线程来支撑大量 Goroutine 的并发。多个 Goroutine 通过用户级别的上下文切换来共享内核线程 M 的计算资源,但对于操作系统来说并没有线程上下文切换产生的性能损耗。
为了更加充分地利用线程的计算资源,Go 调度器采取了以下几种调度策略:
1. 任务窃取(work-stealing)
有的 Goroutine 运行快,有 Goroutine 运行慢。当每个 P 之间的 G 任务不均衡时,调度器允许从 GRQ,或者其他 P 的 LRQ 中获取 G 执行。
2. 减少阻塞
如果正在执行的 Goroutine 阻塞了线程 M 怎么办?P 上 LRQ 中的 Goroutine 会获取不到调度吗?
在 Go 中,阻塞主要分为以下 4 种场景。
场景 1:由于原子、互斥量或通道操作调用导致 Goroutine 阻塞。调度器将把当前阻塞的 Goroutine 切换出去,重新调度 LRQ 上的其他 Goroutine。
场景 2:由于网络请求和 IO 操作导致 Goroutine 阻塞。此时,Go 程序会提供网络轮询器(NetPoller)来处理网络请求和 IO 操作的问题,其后台通过 kqueue(MacOS),epoll(Linux)或 iocp(Windows)来实现 IO 多路复用。通过使用网络轮询器进行网络系统调用,调度器可以防止 Goroutine 在进行这些系统调用时阻塞 M。这可以让 M 执行 P 的 LRQ 中其他的 Goroutines,而不需要创建新的 M。有助于减少操作系统上的调度负载。
场景 3:在调用一些系统方法的过程中发生阻塞。这种情况下,网络轮询器无法使用,而进行系统调用的 Goroutine 将阻塞当前 M。此时,调度器将阻塞的 M 与 P 分离,同时创建新 M 来服务 P。
场景 4:如果在 Goroutine 中执行一个 sleep 操作,导致 M 被阻塞了。 Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以抢占的标识符,别的 Goroutine 就可以抢先进来执行。
只要下次这个 Goroutine 进行函数调用,那么就会被强占,同时也会保护现场,然后重新放入 P 的本地队列里面等待下次执行。
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
该免费文章来自《极客视点》,如需阅读全部文章,
请先领取课程
请先领取课程
免费领取
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(8)
- 最新
- 精选
- 光明Go为什么这么“快” 1、怎么让我们的系统更快 随着信息技术的迅速发展,单台服务器处理能力越来越强,迫使编程模式由从前的串行模式升级到并发模型。 并发模型包含 IO 多路复用、多进程以及多线程,这几种模型都各有优劣,现代复杂的高并发架构大多是几种模型协同使用,不同场景应用不同模型,扬长避短,发挥服务器的最大性能。 而多线程,因为其轻量和易用,成为并发编程中使用频率最高的并发模型,包括后衍生的协程等其他子产品,也都基于它。 2、并发 ≠ 并行 并发 (concurrency) 和 并行 ( parallelism) 是不同的 3、进程、线程、协程 进程:进程是系统进行资源分配的基本单位,有独立的内存空间。 线程:线程是 CPU 调度和分派的基本单位,线程依附于进程存在,每个线程会共享父进程的资源。 协程:协程是一种用户态的轻量级线程。 4、线程上下文切换 上下文切换的代价是高昂的,因为在核心上交换线程会花费很多时间。 如果存在跨核上下文切换(Cross-Core Context Switch),可能会导致 CPU 缓存失效(CPU 从缓存访问数据的成本大约 3 到 40 个时钟周期,从主存访问数据的成本大约 100 到 300 个时钟周期),这种场景的切换成本会更加昂贵。 5、Golang 为并发而生 Goroutine 非常轻量,上下文切换代价小,内存占用小,Golang 程序中可以轻松支持10w 级别的 Goroutine 运行。 6、Go 调度器实现机制: Go 调度器模型我们通常叫做G-P-M 模型,他包括 4 个重要结构,分别是G、P、M、Sched。 G:Goroutine,存储。 P: Processor,表示逻辑处理器 P 的数量由用户设置的 GoMAXPROCS 决定,但是不论 GoMAXPROCS 设置为多大,P 的数量最大为 256。 M: Machine,OS 内核线程抽象,代表着真正执行计算的资源。 M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。 M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。 Sched:Go 调度器,它维护有存储 M 和 G 的队列以及调度器的一些状态信息等。 为了更加充分利用线程的计算资源,G11
- VfeelitGo 很优秀 但是生态还不够好。入坑要谨慎。21
- 小斧场景 1:由于原子、互斥量或通道操作调用导致 Goroutine 阻塞。 场景 2:由于网络请求和 IO 操作导致 Goroutine 阻塞。 场景 3:在调用一些系统方法的过程中发生阻塞。 场景 4:如果在 Goroutine 中执行一个 sleep 操作,导致 M 被阻塞了。1
- 乔帮主还是很年轻的
- AceHan一直不明白为啥大家都说go跑得快,看完这篇文章,就想说一句:原来是这样啊!
- 耿老的竹林这次春节假期把goml语言的专栏学了一遍。目前看只是基础功能接触,需要拿实际项目来练练手。1
- 无根浮盈空欢喜第一次听说GO,很新鲜。
- 极客如果一个协程一直for ;;会调度吗1
收起评论