开发者

详解go语言是如何实现协程的

开发者 https://www.devze.com 2024-08-14 14:33 出处:网络 作者: shark_chili
目录写在文章开头详解协程工作机制和实现协程示例协程实现结构谈谈go语言对于线程的抽象小结写在文章开头www.devze.com
目录
  • 写在文章开头
  • 详解协程工作机制和实现
    • 协程示例
    • 协程实现结构
    • 谈谈go语言对于线程的抽象
  • 小结

    写在文章开头www.devze.com

    go语言的精华就在于协程的设计,只有理解协程的设计思想和工作机制,才能确保我们能够完全的利用协程编写强大的并发程序。

    详解go语言是如何实现协程的

    详解协程工作机制和实现

    协程示例

    正式介绍底层之前,我们给出一段协程的代码示例,可以看到笔者开启一个协程进行函数内部调用:

    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)
    
     go func() {
      foo1()
      defer wg.Done()
     }()
    
     //等待上述协程运行结束
     wg.Wait()
    }
    

    运行结果如下:

    foo1 调用 foo2

    foo2调用foo3

    foo3 执行了

    结合debug我们可以看到当前协程的调用栈帧,在函数调用前插入一个goexit的东西,结合这一点我们开始对协程的深入剖析:

    详解go语言是如何实现协程的

    在这里插入图片描述

    协程实现结构

    go语言的协程结构为:

    • 通过一个stack记录其高地址和低地址。
    • 通过schedsp(即stackpointer)栈帧的指针程序计数器pc(指向下一条运行的指令).
    • 采用goid生成唯一标识。
    • 然后再用atomicstatus记录其执行状态。

    基于这几点我们结合上述的代码给出协程的底层结构,如下图所示,当前协程的stack记录整个foo1函数的高低地址,假设我们当前的协程go来到foo2函数准备调用foo3函数,我们的sched中的sp即stackpointer记录foo2的指针,同时因为foo2内部会调用foo3所以程序计数器pc记录着调用foo3的指令。

    最后因为协程都是由线程调度的,所以协程的内部也有一个变量记录着当前线程的指针m:

    详解go语言是如何实现协程的

    到此我们了解了协程核心结构,同时我们也在runtime2.go这一文件中即给出上述所说的核心变量:

    type g struct {
     //记录栈帧的高地址和低地址
     stack       stack   // offset known to runtime/cgo
     //......
     m         *m //执行当前协程的线程指针
     //记录当前堆栈的指针以及下一条指令的运行地址
     sched     gobuf
     atomicstatus atomic.Uint32
     goid         uint64
     
     //......
    }
    

    步入stack可以看到lohi两个专门记录栈帧高低地址的指针:

    type stack struct {
     lo uintptr
     hi uintptr
    }
    

    对应的我们也给出sched 的类型gobuf,可编程以看到sppythonpc两个核心指针变量:

    type gobuf struct {
     
     sp   uintptr
     pc   uintptr
     //......
    }
    

    谈谈go语言对于线程的抽象

    上文我们提出线程的用m指针记录,如下源码所示,我们都知道在go语言中每个线程都会从一个协程队列中获取协程执行,所以执行时它会用curg记录当前运行的协程,然后通过id对自己进行唯一标识,而mOS则是及记录当前操作系统信息,这其中最核心的就是g0它就是每一个线程的操作调度器:

    type m struct {
     g0      *g     // goroutine with scheduling stack
     id   javascript         int64 
     
     curg          *g       // current running goroutine
     
     mOS
    
    }
    

    了解整体结构之后我们再来聊聊go语言线程的g0栈是如何工作的,如下图所示,每一个g0栈都会通过schedule开始工作:

    • 通过execute从协程队列中获取任务。
    • 调用gogo方法在协程调用前插入go exit指针它记录g0栈帧,这个指针就是用于协程执行退出或者挂起是可以通过这个指针跳回g0栈。
    • 然后就是执行当前协程。
    • 协程执行完成切换回g0栈,重新调用schedule方法再次从步骤1开始执行,由此构成一个循环。

    详解go语言是如何实现协程的

    这里我们也给出asm_amd64.s中关于gogo的汇编代码,可以看到gobuf_sp方法它会记录当前stack pointer也就是我们上文针对g0所说的g0栈地址:

    TEXT gogo<>(SB), NOSPLIT, $0
     get_tls(CX)
     MOVQ DX, g(CX)
     MOVQ DX, R14  // set the g register
     //记录g0栈地址
     MOVQ gobuf_sp(BX), SP // restore SP
     MOVQ gobuf_ret(BX), AX
     MOVQ gobuf_ctxt(BX), DX
     MOVQ gobuf_bp(BX), BP
     MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
     MOVQ $0, gobuf_ret(BX)
     MOVQ $0, gobuf_ctxt(BX)
     MOVQ $0, gobuf_bp(BX)
     MOVQ gobuf_pc(BX), BX
     JMP BX
    

    小结

    自此我们从go语言底层实现的角度完整的剖析的协程与线程的关系和实现,希望对你有帮助。

    以上就是详解go语言是如何实现协程的的详细内容,更多关于go实现协程的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号