面试官:谈谈 Go GMP 中 的 hand off 机制

大家好,我是木川

一、什么是 hand off

Go语言的 hand off 机制 和 work steaing 机制一样,也是一种用于调度协程(Goroutines)的策略,有助于充分利用多核 CPU,提高并发性能,减少线程空转,从而使 Go 程序更高效地运行

hand off 机制 也称为 P 分离机制,核心思想是当线程 M 因为 G 进行的系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的 M 执行

二、分离流程

当 M 执行的 G 发生阻塞操作或者发生了系统调用,M 就会阻塞,如果当前绑定的P还有要执行的 G,  Go runtime 会把这个 P 从 M 摘除,M 进入阻塞(对应下图中的M1),P 分离给其它空闲 M 调度(对应下图中的M2)

当 M 系统调用(或阻塞)结束时,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列,如果获取不到 P,那么这个 G 加入到全局 G 里,这个 M 会加入到空闲线程列表中,重新可以进入调度循环

da5237fb409e0f0971615f2747e97174.png

最后给自己的原创 Go 面试小册打个广告,如果你从事 Go 相关开发,欢迎扫码购买,目前 10 元买断,加下面的微信发送支付截图额外赠送一份自己录制的 Go 面试题讲解视频

97a8543aedcf09536165d40ff48e3784.jpeg

764c1bef284f1b92cbee34c0a5fa4fa3.png

如果对你有帮助,帮我点一下在看或转发,欢迎关注我的公众号

<think>我们正在讨论GolangGMP模型中的工作窃取机制。根据引用[3],当某个P的本地队列为空且全局队列也没有G时,它会尝试从其他P的本地队列中偷取一半的G。具体来说,偷取者会从其他P的本地队列尾部取一半的G,放到自己的本地队列中执行。 另外,引用[4]展示了P的结构体,其中包含一个本地运行队列(runq)以及队列的头尾指针。引用[5]则说明了G(goroutine)的各种状态。 因此,我们可以详细解释工作窃取机制如下: 1. 当某个P(假设为P2)完成了自己本地队列中的所有G,并且全局队列中也没有G时,它会开始工作窃取。 2. 它会随机选择一个其他的P(比如P1),然后从P1的本地队列尾部偷取一半的G。例如,如果P1的本地队列中有N个G,那么偷取的数量为(N/2)向上取整(实际上代码实现中会偷取一半,但具体细节可能更复杂)。 3. 偷取来的G会被放入P2的本地队列中,然后P2就可以开始执行这些G。 这样做的好处是平衡各个P之间的负载,避免某些P空闲而其他P过载。 下面我们结合引用内容详细说明: 引用[3]提到:“全局队列已经没有G,那m就要执行work stealing(偷取):从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。P2从P1的本地队列尾部取一半的G,本例中一半则只有1个G8,放到P2的本地队列并执行。” 引用[4]中P的结构体有一个固定大小的循环队列(runq)来存储G,大小为256。另外还有一个runnext字段,用于存放下一个要执行的G(高优先级)。 工作窃取机制在源码中的实现位于`runtime/proc.go`文件中的`stealWork`函数。但具体细节我们不必深究,只需理解其原理。 总结工作窃取步骤: - 步骤1:P检查自己的本地队列,为空则检查全局队列(需要加锁),全局队列也为空则开始工作窃取。 - 步骤2:随机选择一个P,尝试从其本地队列中偷取一半的G(从队列尾部偷取,这样不会影响该P正在执行的G,因为通常P从队列头部取G执行)。 - 步骤3:如果偷取成功,则将偷来的G放入自己的本地队列,然后开始执行。 - 步骤4:如果偷取失败(其他P的队列也空),则继续随机选择另一个P,直到尝试过所有P(最多尝试四次)。 注意:工作窃取可以避免全局队列的锁竞争,因为每个P的本地队列操作不需要全局锁(除了偷取时需要锁住被偷的P的队列)。 此外,引用[2]提到:“p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合”。工作窃取正是由P来协调的,M(线程)通过绑定的P来获取G。 下面我们尝试用伪代码描述工作窃取的大致逻辑(非真实代码,仅示意): ```go // 当P本地队列为空时,尝试获取G func findRunnable() (gp *g) { // 检查本地队列、全局队列等 // ... if 全局队列为空 { // 尝试工作窃取 for i := 0; i < 4; i++ { // 最多尝试4次 // 随机选择一个P p2 := randomP() // 从p2的本地队列尾部偷取一半的G half := (len(p2.runq) + 1) / 2 // 向上取整 if half > 0 { // 偷取half个G gs := p2.stealFromTail(half) // 将偷来的G放入自己的本地队列 thisP.enqueue(gs) // 取出一个G执行 return thisP.dequeue() } } } // 如果还是没偷到,则进入休眠等 // ... } ``` 实际源码中,偷取操作会考虑队列的平衡,避免一次性偷取过多导致被偷P的负载突然下降。 最后,工作窃取机制是许多并发调度系统中常见的负载均衡策略,在GoGMP模型中,它帮助多个P之间平衡G的执行任务,从而更充分地利用多核CPU资源。</think>### Golang GMP模型中的工作窃取机制详解 在Go语言GMP调度模型中,**工作窃取(Work Stealing)** 是实现负载均衡的核心机制。当某个处理器(P)的本地任务队列为空时,它会从其他繁忙的P中"窃取"一部分Goroutine(G)来执行,从而最大化CPU利用率。以下是详细解析: #### 1. **触发条件** - 当P的本地队列为空(`runq`为空且`runnext`为nil) - 全局队列也没有待执行的G - 此时P会进入工作窃取状态[^3] #### 2. **窃取流程** ```mermaid graph LR A[P空闲] --> B{检查全局队列} B -- 为空 --> C[随机选择目标P] C --> D[从目标P本地队列尾部窃取] D --> E{窃取数量?} E -- 成功 --> F[取目标队列一半G] E -- 失败 --> C F --> G[放入窃取者本地队列] ``` 具体步骤: 1. **随机选择目标P**:从其他活跃P中随机选择一个 2. **尾部窃取**:从目标P本地队列**尾部**取走一半G(例如目标队列有8个G,则窃取4个)[^3] 3. **负载均衡**:优先窃取队列较长的P,避免频繁打扰短队列 4. **失败重试**:最多尝试`4`次随机选择(源码中`stealWork`函数的`max steal tries`) #### 3. **关键设计特点** - **尾部窃取优势**: - 减少锁竞争:目标P通常从队列**头部**取G执行,尾部操作冲突小 - 保持缓存局部性:被窃取的G通常是新创建的,执行状态更完整[^4] - **原子操作**:通过`runqhead/runqtail`的CAS操作避免全局锁[^4] - **窃取量计算**:`n = min(len(targetQ)/2, len(targetQ)-1)` 确保留至少1个G给目标P #### 4. **源码实现要点** - **窃取入口**:`runtime.schedule() → runtime.findrunnable() → runtime.stealWork()` - **队列操作**(参考P结构体[^4]): ```go type p struct { runqhead uint32 // 环形队列头指针 runqtail uint32 // 环形队列尾指针 runq [256]guintptr // 固定大小本地队列 runnext guintptr // 高优先级插队G } ``` - **窃取算法**: ```go // 伪代码简化 func stealWork(nowP) { for i := 0; i < 4; i++ { // 最多尝试4次 targetP := randomP() n := targetP.runqCount() / 2 // 计算窃取量 if n > 0 { gs := targetP.popTail(n) // 原子操作取尾部G nowP.push(gs) // 加入窃取者队列 return } } } ``` #### 5. **性能影响** - **优点**: - 减少线程阻塞:避免M(线程)因无任务而休眠 - 提升并行度:快速平衡各P间负载,尤其适合任务量不均衡的场景 - **限制**: - 全局队列优先级低:只有当所有P本地队列为空时才检查全局队列 - 窃取开销:随机选择目标P可能命中空队列(通过重试机制缓解) #### 6. **设计哲学** 工作窃取机制完美体现了GMP模型的调度原则:**"当本地资源不足时,协作式地获取外部资源,而非被动等待"**。通过P作为中介(如引用[2]所述:"P是GMP的中枢"),实现了G与M的动态解耦,使G能跨M执行[^2]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值