记一次关于goroutine调度的探索

本文记录了一次对Go语言中goroutine调度的深入探索,通过分析源码,解释了三段不同代码的执行逻辑,揭示了goroutine的调度原理,包括runnext的使用以及time.Sleep如何影响调度。通过对源码的学习,加深了对Go语言的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

记一次关于goroutine调度的探索

问题

有以下三段代码,内容大体相同,只有很小的差别。

  • 第一段
	func main() {
	runtime.GOMAXPROCS(0)
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(n int) {
			print(n, " ")
			wg.Done()
		}(i)
	}
	wg.Wait()
}
  • 第二段
func main() {
	runtime.GOMAXPROCS(1)
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(n int) {
			print(n, " ")
			wg.Done()
		}(i)
	}
	wg.Wait()
}
  • 第三段
func main() {
	runtime.GOMAXPROCS(1)
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(n int) {
			time.Sleep(time.Second)
			print(n, " ")
			wg.Done()
		}(i)
	}
	wg.Wait()
}

其中前两段代码是无意中看到的一道go面试题中的,虽然也看到了一些对于二者输出结果的解释,但感觉还是不够清晰,于是自己做了一些尝试,并无意中发现了第三段代码。好,下面先给出这三段代码相应的输出:

  • 第一段:0到4的乱序输出
  • 第二段:4 0 1 2 3
  • 第三段:3 4 0 1 2

探索

  • 第一段的乱序输出是很好理解的,CPU是多核的,创建了多个goroutine之后,每个goroutine得到执行的顺序无法保证,所以会出现0到4的乱序输出。
  • 第二段输出的循序也是很好理解的,由于设置了单核执行,goroutine根据创建顺序依次执行,但问题在于最后创建的goroutine最先执行了。
    一顿搜索查阅之后,知道了大概原因,但总觉得不够清晰,最终找到了以下从P中获取G(go源码中表示goroutine的数据结构,关于G、P、M网上有很多大佬的
<think>嗯,用户想了解Goroutine调度原理,这个问题我之前可能接触过,但需要仔细回忆一下。首先,GoroutineGo语言中的轻量级线程,由Go运行时管理。用户提到的几个引用里有关于调度器的信息,比如P的runqueue,还有gopark和goready函数。我需要把这些信息整合起来,解释清楚调度机制。 首先,Goroutine调度模型是GMP模型,也就是Goroutine、Machine(线程)、Processor。用户提供的引用[2]提到,新建的Goroutine会存储在P的本地队列中,之后可能被移动到全局队列。这可能涉及到工作窃取,当本地队列空了,P会去全局队列或者其他P那里偷任务。这部分需要详细说明。 然后,调度时机,比如系统调用、channel操作、time.Sleep等,这些都会触发调度器让出或恢复Goroutine。引用[3]中的gopark和goready函数应该是在这种情况下被调用的,比如当Goroutine等待时被挂起,完成后又被唤醒。需要解释这些函数的作用和调用场景。 还要注意调度器的抢占式调度,防止某个Goroutine长时间占用线程。Go 1.14之后基于信号的抢占机制,这也是一个要点。需要提到这一点,说明调度器如何保证公平性。 可能用户的问题还包括如何优化Goroutine的使用,或者调试调度相关的问题,比如Goroutine泄漏。不过用户当前的问题是调度原理,所以重点在机制上。 需要确保回答结构清晰,分点讲解GMP模型、调度流程、触发时机、抢占机制等。同时,相关问题的生成要针对调度原理的扩展,比如优化、问题排查、与线程对比等。确保引用正确,引用[1]、[2]、[3]分别对应不同的部分,比如协程简介、本地队列和全局队列、调度函数。 最后检查是否符合用户的要求,比如使用中文,数学公式的格式是否正确,虽然这个问题可能不涉及太多公式,但如果有变量如G、M、P,需要用$符号包裹。还有独立公式是否需要,可能不需要,但如果有的话要使用$$格式。确认回答中的引用标识是否正确添加,比如在提到本地队列时引用[2],在提到gopark时引用[3]。</think>### Goroutine 调度原理 Go 语言的 goroutine 调度采用**G-M-P 模型**(Goroutine-Machine-Processor),通过协作式与抢占式结合的调度策略实现高效并发。以下是核心机制的分步解析: --- #### 一、GMP 模型组成 1. **G (Goroutine)** 轻量级协程,初始栈大小仅 2KB,可动态扩展。通过 `go` 关键字创建,例如: ```go go func() { fmt.Println("Hello") }() ``` 2. **M (Machine)** 操作系统线程(OS Thread),负责执行 G 的代码。M 与内核线程绑定,通过 `runtime.LockOSThread()` 可锁定到当前线程[^1]。 3. **P (Processor)** 逻辑处理器,管理本地 G 队列(`runqueue`)和运行上下文。P 的数量默认等于 CPU 核心数,可通过 `GOMAXPROCS` 调整[^2]。 --- #### 二、调度流程 1. **Goroutine 创建与分配** 新创建的 G 优先放入当前 P 的本地队列。若本地队列满(容量 256),则将半数 G 转移到全局队列(Global Queue)[^2]。 2. **工作窃取(Work Stealing)** 当 P 的本地队列为空时,按以下顺序获取 G: - 从全局队列获取 - 从其他 P 的本地队列“窃取” 此机制平衡负载,避免资源闲置[^2]。 3. **系统调用处理** - 若 G 发起**阻塞式系统调用**(如文件 I/O),M 会与 P 解绑,P 寻找空闲 M 或创建新 M 继续执行其他 G。 - 系统调用结束后,M 尝试关联 P,若失败则将 G 放入全局队列并进入休眠[^1]。 --- #### 三、调度触发时机 1. **主动让出** 调用 `runtime.Gosched()` 主动让出 CPU,G 被放回队列。 2. **通道操作** 当 G 因通道阻塞时,触发调度器切换。 3. **时间片耗尽** Go 1.14+ 引入**基于信号的抢占调度**,防止长时间占用 CPU[^3]。 --- #### 四、关键函数与状态切换 - **`gopark()`** 将 G 置为等待状态(如 `time.Sleep` 或锁等待),并释放关联的 M[^3]。 - **`goready()`** 唤醒等待的 G,将其重新加入运行队列[^3]。 --- #### 性能优化要点 1. **控制 Goroutine 数量** 避免无限制创建,使用 `sync.Pool` 复用对象。 2. **减少阻塞操作** 使用非阻塞 I/O 或异步库(如 `netpoll`)。 3. **绑定 CPU** 通过 `taskset` 或 `numactl` 减少上下文切换。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值