设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间23 现在时间23 缓存数据 轻轻的闭上眼睛,慢慢的酝酿心情,将白天所有烦恼不快撒向天空,随着流星的坠落一起沉淀,愿今夜有个好梦,晚安!

轻轻的闭上眼睛,慢慢的酝酿心情,将白天所有烦恼不快撒向天空,随着流星的坠落一起沉淀,愿今夜有个好梦,晚安!

查看: 1424|回复: 2

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

[复制链接]

  离线 

TA的专栏

  • 打卡等级:无名新人
  • 打卡总天数:2
  • 打卡月天数:0
  • 打卡总奖励:31
  • 最近打卡:2025-04-09 14:46:37
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
32
主题
24
精华
0
金钱
123
积分
60
注册时间
2023-10-3
最后登录
2025-5-31

发表于 2024-4-19 17:42:05 | 显示全部楼层 |阅读模式
目录


  • 顺序执行有什么问题

    • 协程切换
    • 随机抽取全局协程

  • 协程如何并发执行
  • 协程切换时机

    • 主动挂起
    • 系统调用完成后
    • 基于协作的抢占式调度
    • 基于信号的抢占式调度

  • 总结

顺序执行有什么问题

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

协程切换

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

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

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

随机抽取全局协程

在线程循环的
  1. shedule
复制代码
  1. findRunnable
复制代码
函数中,每隔一段时间就会从全局队列中获取一个协程放到本地队列,再通过本地队列的协程切换,使得来自全局队列的协程有机会运行,从而解决全局队列协程的饥饿问题。来看下源码:
  1. if pp.schedtick%61 == 0 && sched.runqsize > 0 {
  2.    lock(&sched.lock)
  3.    gp := globrunqget(pp, 1)
  4.    unlock(&sched.lock)
  5.    if gp != nil {
  6.       return gp, false, false
  7.    }
  8. }
复制代码
  1. pp.schedtick
复制代码
表示线程循环的次数,如果达到61的倍数,就执行
  1. globrunqget
复制代码
,从全局队列中获取协程。

协程如何并发执行

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

协程切换时机

协程的切换时机如下:

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

主动挂起

协程可以调用
  1. runtime.gopark
复制代码
方法,使自己陷入休眠。
2.webp

源码如下:
  1. // 将当前协程置于等待状态
  2. func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
  3.    if reason != waitReasonSleep {
  4.       checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
  5.    }
  6.    mp := acquirem()
  7.    gp := mp.curg
  8.    status := readgstatus(gp)
  9.    if status != _Grunning && status != _Gscanrunning {
  10.       throw("gopark: bad g status")
  11.    }
  12.    mp.waitlock = lock
  13.    mp.waitunlockf = unlockf
  14.    gp.waitreason = reason
  15.    mp.waittraceev = traceEv
  16.    mp.waittraceskip = traceskip
  17.    releasem(mp)
  18.    // can't do anything that might move the G between Ms here.
  19.    mcall(park_m)
  20. }
复制代码
可以看到:

    1. gopark
    复制代码
    中通过
    1. acquirem
    复制代码
    获取到当前的线程指针mp
  • 通过mp获取到当前运行的协程指针gp
  • 给mp,gp的一些字段赋值,修改状态
  • 然后调用
    1. mcall
    复制代码
    1. mcall
    复制代码
    是一个汇编方法,作用时切换到g0栈,并执行传入的函数。这里执行
    1. park_m
    复制代码
    函数,最终跳转到
    1. schedule
    复制代码
    方法,也就是线程循环的开头,实现了协程的主动切换。
  1. // park_m函数最终跳转到schedule
  2. func park_m(gp *g) {
  3.    mp := getg().m
  4.    ...
  5.    schedule()
  6. }
复制代码
由于gopark是小写开头的,外部无法调用。我们在使用
  1. time.Sleep
复制代码
  1. sync.WaitGroup
复制代码
时,会间接的使用到gopark,将协程休眠。

系统调用完成后

当协程要执行读写文件、网络 IO、进程间通信等系统调用的操作时,会进入
  1. entersyscall
复制代码
函数,将该协程暂停并放入等待队列。
当系统调用完成后,由于io操作都比较耗时,说明该协程已经运行了挺长一段时间了,因此将协程挂起,切换另一个协程执行很合理。
3.webp

  1. exitsyscall
复制代码
也位于runtime中,源码部分如下:
  1. func exitsyscall() {
  2.    gp := getg()
  3.    ...
  4.    mcall(exitsyscall0)
  5.    ...
  6. }
复制代码
又是熟悉的
  1. mcall
复制代码
,mcall执行了
  1. exitsyscall0
复制代码
函数,最终跳转到线程循环开头的
  1. schedule
复制代码
函数,完成协程切换。

基于协作的抢占式调度

如果协程既不主动挂起,也没有进行系统调用呢,那就一直切换不出去了?该怎么解决呢,如果每个协程都经常调用同一个方法的话,那就可以在这个方法里加入一个钩子,让这个协程切换出去。
思路有了,具体找哪个方法呢?这里做一个演示。
  1. package main
  2. import (
  3.    "fmt"
  4.    "time"
  5. )
  6. func do1() {
  7.    do2()
  8. }
  9. func do2() {
  10.    do3()
  11. }
  12. func do3() {
  13.    fmt.Println("do3")
  14. }
  15. func main() {
  16.    go do1()
  17.    time.Sleep(time.Hour)
  18. }
复制代码
以上代码开启一个do1协程,do1调用do2,do2调用do3。我们通过
  1. go build -gcflags -S main.go
复制代码
命令,查看汇编代码,发现多次调用到了
  1. runtime.morestack_noctxt
复制代码
方法。在函数跳转的时候,编译器会插入
  1. runtime.morestack_noctxt
复制代码
这个方法。目的是检查函数栈空间是否足够。 简略源码如下:
  1. TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
  2.    MOVL   $0, DX
  3.    JMP    runtime·morestack(SB)
复制代码
  1. TEXT runtime·morestack(SB),NOSPLIT|NOFRAME,$0-0
  2.    ...
  3.    BL runtime·newstack(SB)
  4.    ...
复制代码
最终调用到
  1. newstack
复制代码
这个go方法。
现在对于运行时间超过10ms的大协程,其
  1. g.stackguard0
复制代码
会被赋值为
  1. stackPreempt
复制代码
,意味着该协程要切换出去了。
stackPreempt值为
  1. 0xfffffade
复制代码
  1. // 0xfffffade in hex.
  2. const stackPreempt = uintptrMask & -1314
复制代码
于是在
  1. newstack
复制代码
方法中会判断
  1. g.stackguard0
复制代码
是否为
  1. stackPreempt
复制代码
,是则将该协程切换出去。
  1. func newstack() {
  2.         // 判断是否有抢占信号
  3.         preempt := stackguard0 == stackPreempt
  4.         ...
  5.         if preempt {
  6.                 ...
  7.                 // Act like goroutine called runtime.Gosched.
  8.                 gopreempt_m(gp) // never return
  9.         }
  10.         ...
  11. }
复制代码
  1. func gopreempt_m(gp *g) {
  2.    ...
  3.    goschedImpl(gp)
  4. }
复制代码
  1. func goschedImpl(gp *g) {
  2.    ...
  3.    schedule()
  4. }
复制代码
以上流程总结来说:

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


基于信号的抢占式调度

如果协程不主动挂起,不系统调用,不调用其它函数,只是纯计算的任务,那该如何切换呢?如下:
  1. go func() {
  2.    i := 0
  3.    for {
  4.       i++
  5.    }
  6. }()
复制代码
Go就利用了操作系统通信的方式,通过GC的线程向该协程对应的线程发送信号,触发该线程的切换方法。具体步骤为:

  • 注册
    1. SIGURG
    复制代码
    信号的处理函数
  • GC线程工作时,向该目标线程发送信号
  • 线程接收信号后,触发调度方法
流程图如下:
5.webp

源码分析:
线程接收到操作系统信号,进入
  1. sighandler
复制代码
方法,识别信号为SIGURG,进入
  1. doSigPreempt
复制代码
方法。 之后流程:doSigPreempt -> asyncPreempt -> asyncPreempt2 -> mcall -> gopreempt_m -> goschedImpl。 最终调用schedule方法,回到线程开头,完成协程切换。
具体细节各位可以动手查看下,感悟更多。

总结

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

  • 协程主动挂起,调用
    1. gopark
    复制代码
    函数,使协程主动休眠等待
  • 系统调用完成后,由于io操作挺耗时,代表该协程运行太久了,因此切换协程
  • 基于协作的抢占式调度,协程运行超10ms,就标记为抢占。这时协程在跳转到其它方法时,就把自己切换出去
  • 基于信号的抢占式调度,协程纯自闭,得外部干扰。因此通过GC线程发送信号,触发线程的调度方法
以上就是详解Go如何实现协程并发执行的详细内容,更多关于Go协程并发执行的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
12
积分
4
注册时间
2024-1-3
最后登录
2024-1-3

发表于 2025-2-26 12:49:02 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

  • 打卡等级:即来则安
  • 打卡总天数:28
  • 打卡月天数:0
  • 打卡总奖励:430
  • 最近打卡:2025-03-23 00:18:35
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
477
积分
58
注册时间
2023-1-1
最后登录
2025-3-23

发表于 2025-4-11 11:45:02 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表