详解Go如何实现协程并发执行

 更新时间:2023年08月28日 11:13:40   作者:unitiny  
线程是通过本地队列,全局队列或者偷其它线程的方式来获取协程的,目前看来,线程运行完一个协程后再从队列中获取下一个协程执行,还只是顺序执行协程的,而多个线程一起这么运行也能达到并发的效果,接下来就给给大家详细介绍一下Go如何实现协程并发执行

顺序执行有什么问题

很明显,顺序执行会造成协程的饥饿问题。如果某个大协程挂在线程中运行了十分钟,那么队列中其它协程就一直处于休眠中无法运行,这不公平。如果让某些实时性强的协程饥饿,得不到cpu运行,会影响业务。比如视频弹幕,用户发出一条弹幕,得尽快显示在视频中。若此时协程饥饿,得不到处理,用户体验就差了。

该如何解决呢?简单,让大协程切换出去就可以了。

协程切换

回到线程循环这张图中(在深入考究协程一文中有解释),业务方法这块即线程执行的协程。如果业务方法运行时间过长,则触发协程切换。

  • 对协程:保存该协程运行的情况,然后将该协程放入本地队列队尾,休眠该协程。
  • 对线程:从业务方法中跳出,重新执行 schedule 方法,之后会从本地队列中获取一个新的协程运行。

image.png

但这样只是本地队列的协程切换,全局队列的协程仍会饥饿,该如何解决呢?

随机抽取全局协程

在线程循环的 shedule findRunnable 函数中,每隔一段时间就会从全局队列中获取一个协程放到本地队列,再通过本地队列的协程切换,使得来自全局队列的协程有机会运行,从而解决全局队列协程的饥饿问题。来看下源码:

if pp.schedtick%61 == 0 && sched.runqsize > 0 {
   lock(&sched.lock)
   gp := globrunqget(pp, 1)
   unlock(&sched.lock)
   if gp != nil {
      return gp, false, false
   }
}

pp.schedtick 表示线程循环的次数,如果达到61的倍数,就执行 globrunqget ,从全局队列中获取协程。

协程如何并发执行

从以上可得知,线程通过切换协程的方式,不再顺序的执行协程了,从而达到并发执行协程的效果。这关键在于协程的切换,那协程在什么时候会切换呢?

协程切换时机

协程的切换时机如下:

  • 主动挂起,调用 gopark 函数,使协程主动休眠等待
  • 系统调用完成后,io操作耗时,因此切换协程
  • 基于协作的抢占式调度,协程在跳转到其它方法时,就把自己切换出去
  • 基于信号的抢占式调度,通过发送信号,触发线程的调度方法

主动挂起

协程可以调用 runtime.gopark 方法,使自己陷入休眠。

image.png

源码如下:

// 将当前协程置于等待状态
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
   if reason != waitReasonSleep {
      checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
   }
   mp := acquirem()
   gp := mp.curg
   status := readgstatus(gp)
   if status != _Grunning && status != _Gscanrunning {
      throw("gopark: bad g status")
   }
   mp.waitlock = lock
   mp.waitunlockf = unlockf
   gp.waitreason = reason
   mp.waittraceev = traceEv
   mp.waittraceskip = traceskip
   releasem(mp)
   // can't do anything that might move the G between Ms here.
   mcall(park_m)
}

可以看到:

  1. gopark 中通过 acquirem 获取到当前的线程指针mp
  2. 通过mp获取到当前运行的协程指针gp
  3. 给mp,gp的一些字段赋值,修改状态
  4. 然后调用 mcall mcall 是一个汇编方法,作用时切换到g0栈,并执行传入的函数。这里执行 park_m 函数,最终跳转到 schedule 方法,也就是线程循环的开头,实现了协程的主动切换。
// park_m函数最终跳转到schedule
func park_m(gp *g) {
   mp := getg().m
   ...
   schedule()
}

由于gopark是小写开头的,外部无法调用。我们在使用 time.Sleep sync.WaitGroup 时,会间接的使用到gopark,将协程休眠。

系统调用完成后

当协程要执行读写文件、网络 IO、进程间通信等系统调用的操作时,会进入 entersyscall 函数,将该协程暂停并放入等待队列。

当系统调用完成后,由于io操作都比较耗时,说明该协程已经运行了挺长一段时间了,因此将协程挂起,切换另一个协程执行很合理。

image.png

exitsyscall 也位于runtime中,源码部分如下:

func exitsyscall() {
   gp := getg()
   ...
   mcall(exitsyscall0)
   ...
}

又是熟悉的 mcall ,mcall执行了 exitsyscall0 函数,最终跳转到线程循环开头的 schedule 函数,完成协程切换。

基于协作的抢占式调度

如果协程既不主动挂起,也没有进行系统调用呢,那就一直切换不出去了?该怎么解决呢,如果每个协程都经常调用同一个方法的话,那就可以在这个方法里加入一个钩子,让这个协程切换出去。

思路有了,具体找哪个方法呢?这里做一个演示。

package main
import (
   "fmt"
   "time"
)
func do1() {
   do2()
}
func do2() {
   do3()
}
func do3() {
   fmt.Println("do3")
}
func main() {
   go do1()
   time.Sleep(time.Hour)
}

以上代码开启一个do1协程,do1调用do2,do2调用do3。我们通过 go build -gcflags -S main.go 命令,查看汇编代码,发现多次调用到了 runtime.morestack_noctxt 方法。在函数跳转的时候,编译器会插入 runtime.morestack_noctxt 这个方法。目的是检查函数栈空间是否足够。 简略源码如下:

TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
   MOVL   $0, DX
   JMP    runtime·morestack(SB)
TEXT runtime·morestack(SB),NOSPLIT|NOFRAME,$0-0
   ...
   BL runtime·newstack(SB)
   ...

最终调用到 newstack 这个go方法。

现在对于运行时间超过10ms的大协程,其 g.stackguard0 会被赋值为 stackPreempt ,意味着该协程要切换出去了。

stackPreempt值为 0xfffffade

// 0xfffffade in hex.
const stackPreempt = uintptrMask & -1314

于是在 newstack 方法中会判断 g.stackguard0 是否为 stackPreempt ,是则将该协程切换出去。

func newstack() {
        // 判断是否有抢占信号
        preempt := stackguard0 == stackPreempt
        ...
	if preempt {
		...
		// Act like goroutine called runtime.Gosched.
		gopreempt_m(gp) // never return
	}
        ...
}
func gopreempt_m(gp *g) {
   ...
   goschedImpl(gp)
}
func goschedImpl(gp *g) {
   ...
   schedule()
}

以上流程总结来说:

  • Go对大协程会把g.stackguard0标记为stackPreempt。
  • 在大协程调用其它函数时,会调用newstack判断栈空间,顺便判断该协程是否要切换出去。
  • 要切换则进入gopreempt_m -> goschedImpl -> schedule,最终回到线程循环的开头。

流程图如下:

image.png

基于信号的抢占式调度

如果协程不主动挂起,不系统调用,不调用其它函数,只是纯计算的任务,那该如何切换呢?如下:

go func() {
   i := 0
   for {
      i++
   }
}()

Go就利用了操作系统通信的方式,通过GC的线程向该协程对应的线程发送信号,触发该线程的切换方法。具体步骤为:

  • 注册 SIGURG 信号的处理函数
  • GC线程工作时,向该目标线程发送信号
  • 线程接收信号后,触发调度方法

流程图如下:

image.png

源码分析:

线程接收到操作系统信号,进入 sighandler 方法,识别信号为SIGURG,进入 doSigPreempt 方法。 之后流程:doSigPreempt -> asyncPreempt -> asyncPreempt2 -> mcall -> gopreempt_m -> goschedImpl。 最终调用schedule方法,回到线程开头,完成协程切换。

具体细节各位可以动手查看下,感悟更多。

总结

要使协程并发执行,那各个线程就不能顺序的执行协程,得选择合适的时机将协程切换出去,换另一个协程执行。因此切换时机就特别重要了,所以本篇重点讲解了四种切换方式,分别为:

  • 协程主动挂起,调用 gopark 函数,使协程主动休眠等待
  • 系统调用完成后,由于io操作挺耗时,代表该协程运行太久了,因此切换协程
  • 基于协作的抢占式调度,协程运行超10ms,就标记为抢占。这时协程在跳转到其它方法时,就把自己切换出去
  • 基于信号的抢占式调度,协程纯自闭,得外部干扰。因此通过GC线程发送信号,触发线程的调度方法

以上就是详解Go如何实现协程并发执行的详细内容,更多关于Go协程并发执行的资料请关注脚本之家其它相关文章!

相关文章

  • Golang 关于Gin框架请求参数的获取方法

    Golang 关于Gin框架请求参数的获取方法

    Gin是Go语言的Web框架,提供路由和中间件支持,本文介绍如何使用Gin获取HTTP请求参数,包括URLPath参数、URLQuery参数、HTTPBody参数和Header参数,详解直接获取和绑定到结构体两种方法,帮助开发者高效处理Web请求
    2024-10-10
  • GoLang使goroutine停止的五种方法实例

    GoLang使goroutine停止的五种方法实例

    goroutine是Go并行设计的核心,下面这篇文章主要给大家介绍了关于GoLang使goroutine停止的五种方法,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • Golang实现秒读32GB大文件示例步骤

    Golang实现秒读32GB大文件示例步骤

    这篇文章主要为大家介绍了Golang实现秒读32GB大文件的示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • 利用golang进行OpenCV学习和开发的步骤

    利用golang进行OpenCV学习和开发的步骤

    目前,OpenCV逐步成为一个通用的基础研究和产品开发平台,下面这篇文章主要给大家介绍了关于利用golang进行OpenCV学习和开发的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2018-09-09
  • 如何使用Go语言实现远程执行命令

    如何使用Go语言实现远程执行命令

    远程执行命令最常用的方法就是利用SSH协议,将命令发送到远程机器上执行,并获取返回结果。本文将介绍如何使用Go语言实现远程执行命令。下面一起来看看。
    2016-08-08
  • Golang反射获取结构体的值和修改值的代码示例

    Golang反射获取结构体的值和修改值的代码示例

    这篇文章主要给大家介绍了golang反射获取结构体的值和修改值的代码示例及演示效果,对我们的学习或工作有一定的帮助,感兴趣的同学可以参考阅读本文
    2023-08-08
  • 拦截信号Golang应用优雅关闭的操作方法

    拦截信号Golang应用优雅关闭的操作方法

    这篇文章主要介绍了拦截信号优雅关闭Golang应用,本文介绍了信号的概念及常用信号,并给出了应用广泛的几个示例,例如优雅地关闭应用服务、在命令行应用中接收终止命令,需要的朋友可以参考下
    2023-02-02
  • 使用Golang搭建web服务的实现步骤

    使用Golang搭建web服务的实现步骤

    本文主要介绍了使用Golang搭建web服务的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • go语言中的udp协议及TCP通讯实现示例

    go语言中的udp协议及TCP通讯实现示例

    这篇文章主要为大家介绍了go语言中的udp协议及TCP通讯的实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04
  • 简单聊聊Go语言中空结构体和空字符串的特殊之处

    简单聊聊Go语言中空结构体和空字符串的特殊之处

    在日常的编程过程中,大家应该经常能遇到各种”空“吧,比如空指针、空结构体、空字符串等,本文就以 Go 语言为例,一起来看看空结构体和空字符串在 Go 语言中的特殊之处吧
    2024-03-03

最新评论