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看到morestacknewstack的代码,而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.gosighandler方法得以印证,可以看到它会判断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.goasyncPreempt2调用到和上文函数调用抢占式调度方法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语言中协程抢占式调度的所有内容,希望对你有帮助。

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

参考

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

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

本文使用 markdown.com.cn 排版

相关推荐
uzong5 小时前
技术故障复盘模版
后端
GetcharZp5 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程6 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研6 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi6 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国7 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy7 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack8 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9658 小时前
pip install 已经不再安全
后端
寻月隐君8 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github