golang上下文切换测试


引入

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"
)

// 测试 Goroutine 切换的开销,使用 runtime.Gosched()
func BenchmarkGoroutineSwitchWithGosched(b *testing.B) {
// 运行 b.N 次,模拟 Goroutine 切换
for i := 0; i < b.N; i++ {
runtime.Gosched() // 主动触发 Goroutine 切换
}
}

func BenchmarkThreadSwitchWithGosched(b *testing.B) {
// 运行 b.N 次,模拟 Goroutine 切换
runtime.GOMAXPROCS(2)
wg := sync.WaitGroup{}
run := func() {
tid := unix.Gettid() // 获取线程 ID
b.Logf("Goroutine running on thread ID: %d", tid)
runtime.LockOSThread() //锁定当前协程到当前线程上
for i := 0; i < b.N; i++ {
runtime.Gosched() // 主动触发 Goroutine 切换
}
wg.Done()
}
wg.Add(1)
go run()
wg.Add(1)
go run()

wg.Wait()
b.Logf("Goroutine running done")
}
go test -bench .

测试结果

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

Author: deepwzh
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source deepwzh !
  TOC