Golang Go语言中 Goroutine 的抢占机制
Golang Go语言中 Goroutine 的抢占机制
翻译自:Go: Goroutine and Preemption
ℹ︎本文内容基于 Go 1.13 版本。
Go 语言利用内部的调度器管理 goroutine。这个调度器致力于对 goroutine 进行切换,确保它们都能够获得执行时间。不过,调度器有时会抢占这些 goroutine 的运行时间以保证正确的轮换。
调度器和抢占机制
让我们使用一个简单的例子来说明调度器是如何工作的:为了便于阅读,这些例子不会使用原子操作。
func main() {
var total int
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for j := 0; j < 1000; j++ {
total += readNumber()
}
wg.Done()
}()
}
wg.Wait()
}
//go:noinline
func readNumber() int {
return rand.Intn(10)
}
下面是追踪信息( tracing ):
我们可以清楚地观察到调度器控制 goroutine 在处理器上进行轮换,对它们全部给予相应的执行时间。当 goroutine 由于系统调用,channel 阻塞,睡眠,等待互斥量等操作而停止时,Go 会对其进行调度。在上一个例子中,调度器利用了数字生成器中的互斥量,从而给所有 goroutine 执行时间。在追踪信息中也是可以看到的:
不过,如果 goroutine 自身没有任何的停滞,Go 还是需要有办法停止正在运行的 goroutine。这种行为被称作抢占( preemption ),它允许调度器对 goroutine 的执行进行切换。任何执行时间超过 10 毫秒的 goroutine 会被标记为可抢占。而后,抢占行为会发生在函数调用的开始阶段,goroutine 调用栈增加的时候。
让我们来看一个例子,它与上个例子的区别在于去除了数字生成器中的锁:
func main() {
var total int
var wg sync.WaitGroup
for i := gen(0); i < 20; i++ {
wg.Add(1)
go func(g gen) {
for j := 0; j < 1e7; j++ {
total += g.readNumber()
}
wg.Done()
}(i)
}
wg.Wait()
}
var generators [20]*rand.Rand
func init() {
for i := int64(0); i < 20; i++ {
generators[i] = rand.New(rand.NewSource(i).(rand.Source64))
}
}
type gen int
//go:noinline
func (g gen) readNumber() int {
return generators[int(g)].Intn(10)
}
这里是追踪信息:
而且,goroutine 是在函数调用开始阶段被抢占的:
这个检查过程是由编译器自动加入的;这里有一段上例生成的汇编代码:
通过将指令插入在每个函数执行前,这个 runtime 调用确保栈可以增加。同时使得调度器在必要时可以运行。
绝大多数情况下,goroutine 都会给调度器对它们进行调度的能力。但是,一个没有函数调用的循环却可以阻塞调度。
更多关于Golang Go语言中 Goroutine 的抢占机制的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
本质上还是主动 yield,编译器帮你插指令而已
更多关于Golang Go语言中 Goroutine 的抢占机制的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
请教一下,那个时序图是怎么搞出来的?
go tool trace
https://juejin.im/post/5d27400151882530af139a85
文章说的已经过时了,1.14 可以用信号来抢占
在Golang(Go语言)中,Goroutine 是其并发编程的核心抽象,提供了一种轻量级的方式来并发执行任务。为了高效地管理这些并发任务,Go运行时实现了Goroutine的调度机制,其中就包括抢占机制。
抢占机制主要用于解决长时间运行的Goroutine占用CPU资源,导致其他Goroutine饥饿的问题。在没有抢占机制的情况下,如果一个Goroutine进入了一个长时间运行的函数(如一个无限循环),它将一直占用CPU,直到函数返回或主动让出CPU。
Go 1.18及以后的版本中,引入了基于协作和信号量的抢占机制。这种机制的核心思想是,在函数调用的过程中,Go运行时会在某些特定的点(如函数调用、循环迭代等)插入检查点,这些检查点会检查当前Goroutine是否应该被抢占。
当一个Goroutine应该被抢占时,Go运行时会暂停该Goroutine的执行,并将其保存到运行队列的末尾,然后调度其他Goroutine执行。这样,就实现了资源的公平分配,避免了单个Goroutine长时间占用CPU的问题。
需要注意的是,抢占机制虽然提高了资源的利用率和公平性,但也会引入一定的性能开销。因此,在编写Go程序时,开发者仍然需要尽量避免编写过长时间运行的函数,以充分利用Goroutine的优势,实现高效的并发编程。