引入 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 mainimport ( "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