go语言如何实现协程的抢占式调度的?

写在文章开头

go语言通过GMP模型实现协程并发,为了避免单协程持续持有线程导致线程队列中的其他协程饥饿问题,设计者提出了一个抢占式调度机制,本文会基于一个简单的代码示例对抢占式调度过程进行深入讲解剖析。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。

详解协程抢占式调度

函数调用间进行抢占式调度

假设我们现在有这样一个协程,它会进行函数嵌套调用,代码如下所示:

scss 复制代码
func foo1() {
 fmt.Println("foo1调用foo2")
 foo2()
}

func foo2() {
 fmt.Println("foo2调用foo3")
 foo3()
}

func foo3() {
 fmt.Println("foo3")
}

func main() {
 //设置WaitGroup等待协程运行结束
 var wg sync.WaitGroup
 wg.Add(1)
 //通过协程调用foo1
 go func() {
  defer wg.Done()
  foo1()
 }()
 //等待协程运行结束
 wg.Wait()
}

我们给出运行结果:

复制代码
foo1调用foo2
foo2调用foo3
foo3        

基于这段代码示例,我们通过这段指令获取plan9汇编码:

go 复制代码
go build -gcflags -S main.go

可以看到在foo1插入runtime.morestack_noctxt方法,该方法是用于检查当前协程是否有足够的堆栈空间以保证函数的正常调用,基于这一点,go就会在进行这部检查时顺带检查协程的执行时长,一旦超过10ms该方法就会将协程设置为标记可被抢占:

less 复制代码
 0x0061 00097 (F:\github\test\main.go:8) CALL    runtime.morestack_noctxt(SB)

如下图,我们的调用的函数都会被插入一个morestack通过这个标记判断当前协程执行耗时,一旦发现超过10ms则会直接通过抢占式调度的方法g0协程直接调用schedule方法获取另外的协程进行调用:

这一点我们可以在asm_amd64.s看到morestack的newstack的代码,而newstack就是实现抢占式调度的核心:

scss 复制代码
TEXT runtime·morestack(SB),NOSPLIT,$0-0
 // Cannot grow scheduler stack (m->g0).
 get_tls(CX)
 MOVQ g(CX), BX
 MOVQ g_m(BX), BX
 MOVQ m_g0(BX), SI
 CMPQ g(CX), SI
 JNE 3(PC)
 CALL runtime·badmorestackg0(SB)
 CALL runtime·abort(SB)

    //......
    //函数调用前会调用newstack进行抢占式的检查
 CALL runtime·newstack(SB)
 CALL runtime·abort(SB) // crash if newstack returns
 RET

上述的newstack方法在stack.go中,如果当前协程可被抢占则会调用gopreempt_m回到g0调用schedule方法从协程队列中拿到新的协程执行任务:

scss 复制代码
func newstack() {
 preempt := stackguard0 == stackPreempt


 //如果preempt 为true,则直接当前协程被标记为抢占直接调用gopreempt_m让出线程执行权
 if preempt {
  if gp == thisg.m.g0 {
   throw("runtime: preempt g0")
  }
  //......

  // Act like goroutine called runtime.Gosched.
  gopreempt_m(gp) // never return
 }
}

基于系统调用发起信号的抢占式调度

假设我们的协程没有进行额外的函数调用,是否就意味着当前协程的线程不能被抢占呢?很明显不是这样:

  1. 网络传输过程中需要发送某些紧急消息希望通过已有连接迅速将消息通知给对端时,就会产生SIGURG信号,go语言就会在收到此信号时触发抢占式调度。
  1. 进行GC工作时像目标线程发送信号由此实现抢占式调度。

对于第一点我们可以在signal_unix.go的sighandler方法得以印证,可以看到它会判断sig 是否为_SIGURG若是则调用doSigPreempt进行抢占式调度

scss 复制代码
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
 
 //如果传入的信号为_SIGURG则调用doSigPreempt回到schedule实现抢占式调度
 if sig == sigPreempt && debug.asyncpreemptoff == 0 && !delayedSignal {
  // Might be a preemption signal.
  doSigPreempt(gp, c)
  
 }
 //......
}

doSigPreempt会通过调用asyncPreempt最终执行到preempt.go的asyncPreempt2调用到和上文函数调用抢占式调度方法gopreempt_m回到schedule方法从而完成抢占式调度:

scss 复制代码
func doSigPreempt(gp *g, ctxt *sigctxt) {
 //......
 if wantAsyncPreempt(gp) {
  if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
   // 调用asyncPreempt内部会得到一个和上文函数调用时抢占式调度的方法gopreempt_m的调用从而回到schedule方法
   ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
  }
 }

 //......
}

小结

以上便是笔者关于go语言中协程抢占式调度的所有内容,希望对你有帮助。

我是 sharkchili ,CSDN Java 领域博客专家 ,开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。

参考

TCP 带外数据(即紧急模式的发送和接受) :blog.csdn.net/liushengxi_...

Linux(程序设计):59---SIGHUP、SIGPIPE、SIGURG信号处理(附SIGURG信号处理普通数据与外带数据案例):blog.51cto.com/u_15346415/...

本文使用 markdown.com.cn 排版

相关推荐
不才不才不不才17 分钟前
Spring 源码系列(29): 20 道高频面试题源码级解析合集
java·后端·spring
索隆zoro21 分钟前
Army 的可插拔架构:army-jdbc 与方言模块
java·后端
小蒜学长1 小时前
校园社团招新网站的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·校园社团招新网站
蜗牛互联网2 小时前
从Barclays扩大Claude部署看受监管软件工程的责任重分配
java·大数据·人工智能·后端·软件工程
周杰伦fans3 小时前
C#对话摘要降低Token消耗
人工智能·后端·c#
做系统的大强4 小时前
脚本说 PASS、OS 读全零 —— 到底是谁在撒谎?
后端·操作系统
code2cat5 小时前
【随笔】MCP资源更新订阅:通知到达以后,Agent怎样刷新旧资料
java·后端·开发工具·ai agent·mcp
Harvil_6 小时前
任务跑一半,API 突然报“对话超长“断流:我给 Agent 修的紧急逃生通道
后端
用户EasyAdminBlazor6 小时前
EasyAdminBlazor SignalR 实时消息源码解析:从 NotificationHub 到站内消息
后端
程序猿乐锅6 小时前
【黑马点评 | 第十一篇】关注 Feed 流实现
java·数据库·redis·分布式·后端·缓存·maven