GPM解释

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • 1.G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
  • 2.P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • 3.M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

<think>嗯,用户问的是在Go语言中,当遇到channel阻塞时,GPM模型是怎么处理的,这应该是一个面试常见问题。我需要先回顾一下GPM的基本概念,然后重点解释channel阻塞时的调度过程。 首先,GPM指的是Goroutine、Processor、Machine。用户可能已经了解GPM的基本结构,但需要详细说明在channel阻塞时的具体处理机制。我记得当goroutine在channel上发送或接收数据时,如果没有对应的接收或发送方,就会阻塞。这时候,调度器会介入,将阻塞的G移出当前运行的M,并让M继续执行其他G。 具体来说,当G因为channel操作阻塞时,runtime会将这个G的状态从运行中改为等待,并将其从P的本地队列中取出,放到channel的等待队列里。此时,P会解除与当前M的绑定,M可能去寻找其他可运行的G,或者进入休眠状态。而P则会尝试关联其他的M来继续执行本地队列中的其他G。当channel的另一端有数据到达时,等待的G会被重新唤醒,放回到某个P的队列中等待执行。 需要注意的细节包括:阻塞时如何保存上下文,如何切换G,以及唤醒后的调度策略。可能还需要提到work-stealing机制,当P的本地队列为空时,如何从全局队列或其他P那里窃取任务。此外,不同版本的Go可能在调度策略上有细微差别,比如1.14之后的抢占式调度是否会影响channel阻塞的处理。 用户可能需要一个结构清晰的回答,分步骤或分点说明处理流程,最好能结合示例或流程图。同时,面试中可能会追问与系统调用阻塞的区别,或者如何避免频繁阻塞导致的性能问题,需要做好准备。要确保术语准确,比如正确使用G、P、M这些术语,并解释清楚它们各自的角色。</think>以下是针对Go语言中channel阻塞时GPM处理机制的详细解析,适合面试场景: --- ### **一、channel阻塞场景分类** 1. **发送阻塞** ```go ch <- data // 当channel缓冲区满/无接收方时阻塞 ``` 2. **接收阻塞** ```go data := <-ch // 当channel缓冲区空/无发送方时阻塞 ``` --- ### **二、GPM处理流程(核心四步)** ```mermaid graph TD A[G执行channel操作] --> B{是否立即完成?} B -->|是| C[继续执行] B -->|否| D[挂起G到等待队列] D --> E[M释放当前P] E --> F[P寻找新G执行] F --> G[数据就绪后唤醒G] ``` #### **1. 阻塞发生时** - **保存上下文** 将G的执行状态(寄存器值、程序计数器等)保存到`g.sched`结构 - **关联等待队列** 将G加入channel的**发送等待队列**或**接收等待队列** - **修改G状态** 标记G为`Gwaiting`状态 #### **2. P/M调度** - **M释放绑定** ```go m.curg = nil // 解除当前运行的G gp.m = nil // 解除G与M的关联 ``` - **P重新调度** P立即执行`schedule()`函数,从本地队列/全局队列/其他P窃取新G执行 #### **3. 唤醒机制** 当channel数据就绪时: 1. 从等待队列取出第一个G 2. 将G状态改为`Grunnable` 3. 将G放入**原P的本地队列**(优先)或**全局队列** --- ### **三、底层实现细节** #### **1. 关键数据结构** ```go type hchan struct { qcount uint // 当前数据量 dataqsiz uint // 缓冲区大小 buf unsafe.Pointer // 环形缓冲区指针 sendx uint // 发送索引 recvx uint // 接收索引 recvq waitq // 接收等待队列 sendq waitq // 发送等待队列 lock mutex // 互斥锁 } ``` #### **2. 阻塞恢复过程** ```go // 发送端唤醒示例 func chansend(c *hchan, ep unsafe.Pointer, ...) { if c.recvq.dequeue() != nil { // 直接拷贝数据到等待接收的G send(c, sg, ep) goready(sg.g) // 将接收G置为可运行 } else { // 加入发送等待队列 gopark(chanparkcommit, ...) } } ``` --- ### **四、与其他阻塞场景对比** | 阻塞类型 | 处理方式 | 调度开销 | |---------------|-----------------------------------|--------| | channel阻塞 | G入channel队列,M立即重新调度 | 低 | | 系统调用阻塞 | M与P解绑,需等待OS返回 | 高 | | 网络I/O阻塞 | 通过NetPoller异步处理 | 中 | | time.Sleep | 加入定时器堆,到期后唤醒 | 低 | --- ### **五、高频面试问题** 1. **为什么channel阻塞不会导致M阻塞?** - 通过`gopark`将G移出运行队列,M立即执行其他G - 对比:系统调用会导致M阻塞,需要解绑P 2. **如何避免channel导致的协程泄漏?** - 使用`select+default`实现非阻塞操作 - 通过`context.WithTimeout`设置超时 ```go select { case ch <- data: case <-time.After(1*time.Second): // 超时处理 } ``` 3. **channel阻塞与互斥锁阻塞的区别?** - channel阻塞会触发G切换,提升CPU利用率 - 互斥锁阻塞时G保持运行状态(自旋锁),浪费CPU周期 --- ### **六、实际案例分析** ```go func worker(ch chan int) { for { data := <-ch // 接收阻塞点 process(data) } } func main() { ch := make(chan int, 3) go worker(ch) // G1 for i := 0; i < 10; i++ { ch <- i // 缓冲区满时发送阻塞 } } ``` - 当缓冲区满时,发送G被加入`ch.sendq`,M立即执行其他G - 当worker处理完一个数据后,会唤醒最早被阻塞的发送G --- 掌握这些机制可以深入理解Go并发模型的设计哲学,在面试中建议结合《Go语言设计与实现》等参考资料,配合`runtime`包源码分析进行更深入的探讨。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值