引入
golang是一门高性能的编程语言,基于其轻量,高效的的Goroutine的设计,可以实现很小的协程切换开销。
本文将解释一下基本的协程相关的原理,并实际做一个小的测试,去得到其实际的切换开销
原理
协程
协程(Coroutine)是一种更轻量级的并发编程方式,他是在用户态实现的,相比于线程进程更加轻量,拥有更小的上下文切换开销,栈空间等等。
协程主要分为两种类型:
- 有栈协程:以Golang为代表
- 无栈协程:以c++(cpp20协程),python(asyncio)为代表
特性 |
有栈协程 |
无栈协程 |
栈空间 |
协程拥有自己的独立栈,可以进行任意深度的嵌套调用和挂起。 |
协程不拥有独立栈,通常基于状态机实现,挂起点有限。 |
切换时机 |
任意位置进行切换 |
挂起点进行切换 |
切换开销 |
开销相对较高 |
开销相对较低,几乎接近于函数调用开销 |
内存占用 |
固定或浮动大小栈内存(如Goroutine是最小2kb) |
没有栈内存,开销小 |
Goroutine是有栈协程,因此还是存在一定的切换开销,另外其栈是在堆空间分配的连续一块内存,最小是2kb,不够用会选一个新的地方拷贝扩容
GMP模型
golang runtime采用了GMP模型去进行协程的调度,具体为:
G: Goroutine
M: Machine,对应操作系统的线程
P: Processor,逻辑处理器
大体的流程如下:
- G创建后加入到本地运行队列或者全局运行队列,并尝试唤醒一个P去执行
- M启动后,不停的寻找一个P,并运行他上边的G
- 如果M上找得到P,但是P上没有任务,也会把P休眠
- 如果M找不到P,则会将自己休眠
- 后台会运行一个sysmon, 去抢占运行时间过长的G
根据原理,G的切换有几种情况:
- 两个G在同一个M上:此时协程切换的开销比较小
- 两个G在不同M上:此时协程切换的开销等价于线程切换的开销
测试
测试代码
下面的测试代码有两个测试任务:
- BenchmarkGoroutineSwitchWithGosched: 不断的让协程通过runtime.Gosched触发主动切出切入
- BenchmarkThreadSwitchWithGosched: 两个协程分别运行在两个线程上,他们互相进行切换
package main
import ( "runtime" "sync" "testing"
"golang.org/x/sys/unix" )
func BenchmarkGoroutineSwitchWithGosched(b *testing.B) { for i := 0; i < b.N; i++ { runtime.Gosched() } }
func BenchmarkThreadSwitchWithGosched(b *testing.B) { runtime.GOMAXPROCS(2) wg := sync.WaitGroup{} run := func() { tid := unix.Gettid() b.Logf("Goroutine running on thread ID: %d", tid) runtime.LockOSThread() for i := 0; i < b.N; i++ { runtime.Gosched() } wg.Done() } wg.Add(1) go run() wg.Add(1) go run()
wg.Wait() b.Logf("Goroutine running done") }
|
测试结果
goos: linux goarch: amd64 pkg: go-test/context-switch cpu: 13th Gen Intel(R) Core(TM) i7-13700K BenchmarkGoroutineSwitchWithGosched-24 12298116 97.66 ns/op BenchmarkThreadSwitchWithGosched-24 47210 25405 ns/op --- BENCH: BenchmarkThreadSwitchWithGosched-24 main_test.go:25: Goroutine running on thread ID: 1397207 main_test.go:25: Goroutine running on thread ID: 1397208 main_test.go:38: Goroutine running done main_test.go:25: Goroutine running on thread ID: 1397204 main_test.go:25: Goroutine running on thread ID: 1397202 main_test.go:38: Goroutine running done main_test.go:25: Goroutine running on thread ID: 1397209 main_test.go:25: Goroutine running on thread ID: 1397206 main_test.go:38: Goroutine running done main_test.go:25: Goroutine running on thread ID: 1397256 ... [output truncated] testing: BenchmarkThreadSwitchWithGosched-24 left GOMAXPROCS set to 2 PASS ok go-test/context-switch 2.761s
|
根据测试结果,可以得到在作者的配置上边,可以得到:
类别 |
耗时 |
协程切换(同一线程) |
97.66ns |
协程切换(不同线程) |
25.405us |