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

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

详解协程抢占式调度

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

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

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 build -gcflags -S main.go

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

 0x0061 00097 (F:\github\test\main.go:8) CALL    runtime.morestack_noctxt(SB)

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

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

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方法从协程队列中拿到新的协程执行任务:

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
 }
}

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

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

网络传输过程中需要发送某些紧急消息希望通过已有连接迅速将消息通知给对端时,就会产生SIGURG信号,go语言就会在收到此信号时触发抢占式调度。

进行GC工作时像目标线程发送信号由此实现抢占式调度。

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

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方法从而完成抢占式调度:

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语言如何实现协程的抢占式调度的的文章就介绍到这了,更多相关go协程抢占式调度内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang设计模式之原型模式详细讲解

    Golang设计模式之原型模式详细讲解

    如果一个类的有非常多的属性,层级还很深。每次构造起来,不管是直接构造还是用建造者模式,都要对太多属性进行复制,那么有没有一种好的方式让我们创建太的时候使用体验更好一点呢? 今天的文章里就给大家介绍一种设计模式,来解决这个问题
    2023-01-01
  • Golang限流库与漏桶和令牌桶的使用介绍

    Golang限流库与漏桶和令牌桶的使用介绍

    这篇文章主要介绍了golang限流库以及漏桶与令牌桶的实现原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-03-03
  • Golang连接池的几种实现案例小结

    Golang连接池的几种实现案例小结

    这篇文章主要介绍了Golang连接池的几种实现案例小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • go语言Timer计时器的用法示例详解

    go语言Timer计时器的用法示例详解

    Go语言的标准库里提供两种类型的计时器Timer和Ticker。这篇文章通过实例代码给大家介绍go语言Timer计时器的用法,代码简单易懂,感兴趣的朋友跟随小编一起看看吧
    2020-05-05
  • Go prometheus metrics条目自动回收与清理方法

    Go prometheus metrics条目自动回收与清理方法

    这篇文章主要为大家介绍了Go prometheus metrics条目自动回收与清理方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • Go并发之RWMutex的源码解析详解

    Go并发之RWMutex的源码解析详解

    RWMutex是一个支持并行读串行写的读写锁。RWMutex具有写操作优先的特点,写操作发生时,仅允许正在执行的读操作执行,后续的读操作都会被阻塞。本文就来从源码解析一下RWMutex的使用
    2023-03-03
  • 轻松管理多个Go版本之g工具安装与使用方法

    轻松管理多个Go版本之g工具安装与使用方法

    这篇文章主要介绍了轻松管理多个Go版本之g工具安装与使用的相关资料,g是一个Linux、macOS、Windows下的命令行工具,可以提供一个便捷的多版本go环境的管理和切换,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-11-11
  • golang waitgroup辅助并发控制使用场景和方法解析

    golang waitgroup辅助并发控制使用场景和方法解析

    Golang 提供了简洁的 go 关键字来让开发者更容易的进行并发编程,同时也提供了 WaitGroup 对象来辅助并发控制,今天我们就来分析下 WaitGroup 的使用方法,顺便瞧一瞧它的底层源码
    2023-09-09
  • Golang并发控制的三种实现方法

    Golang并发控制的三种实现方法

    在Golang中,有多种方式可以进行并发控制,本文详细的介绍了三种实现方法,Channel优点是实现简单,清晰易懂,WaitGroup优点是子协程个数动态可调整,Context 优点是对子协程派生出来的孙子协程的控制,缺点是相对而言的,要结合实例应用场景进行选择
    2023-08-08
  • golang实现RPC模块的示例

    golang实现RPC模块的示例

    本文详细介绍了在Go语言中如何实现RPC模块,包括RPC服务端和客户端的构建及代码实现,同时提到了使用JSON-RPC的方法,通过简单的步骤,可以实现跨进程的远程过程调用,感兴趣的可以了解一下
    2024-09-09

最新评论