引入
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 |