前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你觉得 Golang 在什么时候会抢占 P?

你觉得 Golang 在什么时候会抢占 P?

原创
作者头像
golang开发者
发布2024-10-28 16:23:41
1010
发布2024-10-28 16:23:41
举报
文章被收录于专栏:go开发者

在 Go 语言中,Goroutine 是并发模型的核心,而 P(Processor) 是 Go 调度器中的一个关键抽象。理解 Goroutine 调度模型 中的 G(Goroutine)M(Machine,内核线程)P(Processor,逻辑处理器) 的关系可以帮助我们理解 Go 的抢占式调度策略。

Go 调度器使用 G-M-P 模型

  • Goroutine (G):一个 Goroutine 代表一个 Go 协程。
  • Processor (P)P 是逻辑处理器,负责调度和管理 Goroutine,最多有 GOMAXPROCSP。每个 P 可以运行一个 Goroutine
  • Machine (M)M 是操作系统的内核线程。每个 M 需要绑定一个 P 来执行 Goroutine

Go 中的抢占式调度

Go 的调度器采用的是协作式调度为主,抢占式调度为辅。协作式调度意味着 Goroutine 需要主动放弃控制权来让其他 Goroutine 运行,比如调用系统调用或者 Goroutine 自己调用 runtime.Gosched()

抢占式调度则是为了防止某些 Goroutine 占用 CPU 太久(比如某个 Goroutine 在长时间执行计算密集型任务),Go 1.14 引入了针对 计算密集型 Goroutine抢占式调度。抢占式调度可以在以下场景下触发:

  • Goroutine 执行时间过长,特别是没有主动进行系统调用、调度让出等行为时。
  • Goroutine 执行在较长的函数调用链上,或者在一些函数的栈帧扩展时(例如深度递归调用或大数组操作时)。
抢占 P 的时机
  1. 系统调用 (syscall) 后:当 Goroutine 执行系统调用后,Goroutine 会让出 P,此时调度器可能会选择调度其他的 Goroutine 来运行。
  2. 垃圾回收 (GC) 阶段:当触发垃圾回收时,调度器会在合适时机抢占 Goroutine,确保 GC 可以进行。
  3. 计算密集型任务被长时间运行:从 Go 1.14 开始,调度器会定期检查长时间运行的 Goroutine,并进行抢占。

代码示例:抢占式调度与长时间运行的 Goroutine

下面的例子展示了一个 Goroutine 在执行计算密集型任务时如何可能会被 Go 的抢占式调度机制打断。

代码语言:go
复制
package main

import (
	"fmt"
	"runtime"
	"time"
)

// 模拟一个计算密集型任务
func busyLoop() {
	for i := 0; i < 1e10; i++ {
		// 占用 CPU,但没有主动让出调度权
	}
	fmt.Println("Finished busy loop")
}

func main() {
	runtime.GOMAXPROCS(1) // 设置只有 1 个 P

	go func() {
		for {
			fmt.Println("Running another goroutine...")
			time.Sleep(500 * time.Millisecond) // 每 500 毫秒休息一次
		}
	}()

	busyLoop() // 执行计算密集型任务

	time.Sleep(2 * time.Second)
}
代码解析:
  1. runtime.GOMAXPROCS(1):我们将 GOMAXPROCS 设置为 1,意味着整个程序中只有一个 P,这样所有 Goroutine 都只能在这个 P 上调度。
  2. busyLoop:这是一个计算密集型任务,在没有主动进行系统调用或让出调度权的情况下,循环执行大量的操作,耗尽 CPU 时间。
  3. 抢占:虽然 busyLoop 没有主动让出 CPU,但由于 Go 的抢占式调度机制,调度器可能会在合适的时间点打断 busyLoop,让其他 Goroutine(比如打印 "Running another goroutine..." 的那个 Goroutine)得到执行机会。
输出示例:
代码语言:bash
复制
Running another goroutine...
Running another goroutine...
...
Finished busy loop

我们可以看到,尽管 busyLoop 是一个计算密集型任务,其他的 Goroutine 仍然会间歇性地被调度并执行。这个就是 Go 抢占式调度的效果。

抢占的实现机制

抢占式调度的核心机制是 定期检查 Goroutine 的执行时间。Go 调度器在后台维护一个时间戳,记录 Goroutine 上次被调度的时间。调度器每隔一段时间会检查当前运行的 Goroutine,如果 Goroutine 占用了 CPU 超过一定时间,调度器就会标记这个 Goroutine 需要被抢占,然后调度其他的 Goroutine 来执行。

抢占式调度通过以下方式触发:

  1. 函数调用边界:当 Goroutine 进行函数调用时,Go runtime 会在合适的时机插入抢占检查点。
  2. 栈增长:当 Goroutine 的栈增长(如递归调用导致栈内存增长)时,调度器也会插入抢占检查。
  3. GC 安全点:垃圾回收过程中,调度器也会尝试抢占。

通过代码观察抢占效果

我们可以通过使用 GODEBUG 环境变量,启用抢占式调度的调试日志,观察抢占调度的具体行为。运行如下代码时,启用调试模式:

代码语言:bash
复制
GODEBUG=schedtrace=1000,scheddetail=1 go run main.go
  • schedtrace=1000 表示每隔 1000 毫秒输出一次调度器状态。
  • scheddetail=1 表示输出详细的调度器信息。
输出内容解释

在输出的调试信息中,我们可以看到调度器何时抢占了 Goroutine,何时让出了 P,以及具体的调度行为。调试信息会包括如下内容:

  • idle M:表示某个 M(线程)变成空闲状态。
  • new work:表示调度器找到了新的工作,分配给 P
  • steal work:表示调度器从其他 P 中窃取任务来运行。

最后我们来总结一下

  • Go 的调度器主要基于 协作式调度,但是对于计算密集型任务会通过 抢占式调度 机制防止长时间占用 CPU。
  • 抢占调度在计算密集型 Goroutine、系统调用后、垃圾回收等场景下被触发。
  • Go 1.14 引入了针对长时间运行的 Goroutine 的抢占式调度,使得 Goroutine 不会因为计算密集任务长时间阻塞 CPU。

这使得 Go 语言能更加高效地运行并发程序,避免单个 Goroutine 长时间霸占 CPU,影响其他 Goroutine 的执行。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Go 中的抢占式调度
    • 抢占 P 的时机
    • 代码示例:抢占式调度与长时间运行的 Goroutine
      • 代码解析:
        • 输出示例:
        • 抢占的实现机制
        • 通过代码观察抢占效果
          • 输出内容解释
          • 最后我们来总结一下
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档