目录
- 顺序执行有什么问题
- 协程如何并发执行
- 协程切换时机
- 主动挂起
- 系统调用完成后
- 基于协作的抢占式调度
- 基于信号的抢占式调度
- 总结
顺序执行有什么问题
很明显,顺序执行会造成协程的饥饿问题。如果某个大协程挂在线程中运行了十分钟,那么队列中其它协程就一直处于休眠中无法运行,这不公平。如果让某些实时性强的协程饥饿,得不到cpu运行,会影响业务。比如视频弹幕,用户发出一条弹幕,得尽快显示在视频中。若此时协程饥饿,得不到处理,用户体验就差了。
该如何解决呢?简单,让大协程切换出去就可以了。
协程切换
回到线程循环这张图中(在深入考究协程一文中有解释),业务方法这块即线程执行的协程。如果业务方法运行时间过长,则触发协程切换。
- 对协程:保存该协程运行的情况,然后将该协程放入本地队列队尾,休眠该协程。
- 对线程:从业务方法中跳出,重新执行方法,之后会从本地队列中获取一个新的协程运行。
但这样只是本地队列的协程切换,全局队列的协程仍会饥饿,该如何解决呢?
随机抽取全局协程
在线程循环的
的
函数中,每隔一段时间就会从全局队列中获取一个协程放到本地队列,再通过本地队列的
协程切换,使得来自全局队列的协程有机会运行,从而解决全局队列协程的饥饿问题。来看下源码:
- 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
- }
- }
复制代码表示线程循环的次数,如果达到61的倍数,就执行
,从全局队列中获取协程。
协程如何并发执行
从以上可得知,线程通过切换协程的方式,不再顺序的执行协程了,从而达到
并发执行协程的效果。这关键在于协程的切换,那协程在什么时候会切换呢?
协程切换时机
协程的切换时机如下:
- 主动挂起,调用函数,使协程主动休眠等待
- 系统调用完成后,io操作耗时,因此切换协程
- 基于协作的抢占式调度,协程在跳转到其它方法时,就把自己切换出去
- 基于信号的抢占式调度,通过发送信号,触发线程的调度方法
主动挂起
协程可以调用
方法,使自己陷入休眠。
源码如下:
- // 将当前协程置于等待状态
- 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)
- }
复制代码可以看到:
- 中通过获取到当前的线程指针mp
- 通过mp获取到当前运行的协程指针gp
- 给mp,gp的一些字段赋值,修改状态
- 然后调用,是一个汇编方法,作用时切换到g0栈,并执行传入的函数。这里执行函数,最终跳转到方法,也就是线程循环的开头,实现了协程的主动切换。
- // park_m函数最终跳转到schedule
- func park_m(gp *g) {
- mp := getg().m
- ...
- schedule()
- }
复制代码由于gopark是小写开头的,外部无法调用。我们在使用
,
时,会间接的使用到gopark,将协程休眠。
系统调用完成后
当协程要执行读写文件、网络 IO、进程间通信等系统调用的操作时,会进入
函数,将该协程暂停并放入等待队列。
当系统调用完成后,由于io操作都比较耗时,说明该协程已经运行了挺长一段时间了,因此将协程挂起,切换另一个协程执行很合理。
而
也位于runtime中,源码部分如下:
- func exitsyscall() {
- gp := getg()
- ...
- mcall(exitsyscall0)
- ...
- }
复制代码又是熟悉的
,mcall执行了
函数,最终跳转到线程循环开头的
函数,完成协程切换。
基于协作的抢占式调度
如果协程既不主动挂起,也没有进行系统调用呢,那就一直切换不出去了?该怎么解决呢,如果每个协程都经常调用同一个方法的话,那就可以在这个方法里加入一个钩子,让这个协程切换出去。
思路有了,具体找哪个方法呢?这里做一个演示。
- 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
复制代码命令,查看汇编代码,发现多次调用到了
方法。在函数跳转的时候,编译器会插入
这个方法。目的是检查函数栈空间是否足够。 简略源码如下:
- 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)
- ...
复制代码最终调用到
这个go方法。
现在对于运行时间超过
10ms的大协程,其
会被赋值为
,意味着该协程要切换出去了。
stackPreempt值为
- // 0xfffffade in hex.
- const stackPreempt = uintptrMask & -1314
复制代码于是在
方法中会判断
是否为
,是则将该协程切换出去。
- 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,最终回到线程循环的开头。
流程图如下:
基于信号的抢占式调度
如果协程不主动挂起,不系统调用,不调用其它函数,只是纯计算的任务,那该如何切换呢?如下:
- go func() {
- i := 0
- for {
- i++
- }
- }()
复制代码Go就利用了操作系统通信的方式,通过GC的线程向该协程对应的线程发送信号,触发该线程的切换方法。具体步骤为:
- 注册信号的处理函数
- GC线程工作时,向该目标线程发送信号
- 线程接收信号后,触发调度方法
流程图如下:
源码分析:
线程接收到操作系统信号,进入
方法,识别信号为SIGURG,进入
方法。 之后流程:doSigPreempt -> asyncPreempt -> asyncPreempt2 -> mcall -> gopreempt_m -> goschedImpl。 最终调用schedule方法,回到线程开头,完成协程切换。
具体细节各位可以动手查看下,感悟更多。
总结
要使协程并发执行,那各个线程就不能顺序的执行协程,得选择合适的时机将协程切换出去,换另一个协程执行。因此切换时机就特别重要了,所以本篇重点讲解了四种切换方式,分别为:
- 协程主动挂起,调用函数,使协程主动休眠等待
- 系统调用完成后,由于io操作挺耗时,代表该协程运行太久了,因此切换协程
- 基于协作的抢占式调度,协程运行超10ms,就标记为抢占。这时协程在跳转到其它方法时,就把自己切换出去
- 基于信号的抢占式调度,协程纯自闭,得外部干扰。因此通过GC线程发送信号,触发线程的调度方法
以上就是详解Go如何实现协程并发执行的详细内容,更多关于Go协程并发执行的资料请关注晓枫资讯其它相关文章!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!