go-GMP 协程切换时机 和 协程实战

水军大提督  金牌会员 | 2024-1-20 07:58:39 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 890|帖子 890|积分 2670

当m在执行某个g的时候,g非常耗时,例如一个for循环,每次循环sleep1分钟,循环1000次。
这个例子看似无聊,却是很难解决的,成功的避开了2个系统切换时机。
如果这个时候,一直执行这个g,别的g就会得不到执行,例如有g是处理用户支付的,这样就会造成收钱不积极。
协程饥饿问题

本地队列

本地队列因为 某个G一直 占着M,导致其他G无法执行。
如果占用时间过长的这个G,能让出来M,让别的G也能执行,本地队列循环的着执行,就能解决这个问题。
全局队列

除了本地队列,全局队列也会有这个问题,如果一个新创建的g,放在全局队列中,而现有的p的本地队列都未执行完,则全局队列需要排队很久。
解决办法,每过一段时间,每个本地队列都先来全局队列中取1个,这样就能解决这个问题。
代码实现:
又到了findRunnable()
  1. // Check the global runnable queue once in a while to ensure fairness.
  2. if pp.schedtick%61 == 0 && sched.runqsize > 0 {
  3.         lock(&sched.lock)
  4.         gp := globrunqget(pp, 1)
  5.         unlock(&sched.lock)
  6.         if gp != nil {
  7.                 return gp, false, false
  8.         }
  9. }
复制代码
这个优先级在 本地队列之前。之前看过 globrunqget()中的逻辑,当max为1时候,就只会取一个。
每61次,就去全局队列中拿一个。
解决办法

协程因为独特的数据结构,能能够暂停的,之前协程的本质有介绍过,暂停后,让别的g也开始循环执行。
切换时机

主动挂起

业务方法主动调用gopark 然后,切换协程。
  1.   源码在proc.go中
  2. // Puts the current goroutine into a waiting state and calls unlockf on the
  3. // 让当前的 g 进入 waiting的状态
  4. func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceReason traceBlockReason, traceskip int) {
  5.         if reason != waitReasonSleep {
  6.                 checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
  7.         }
  8.         mp := acquirem()
  9.         gp := mp.curg
  10.         status := readgstatus(gp)
  11.         if status != _Grunning && status != _Gscanrunning {
  12.                 throw("gopark: bad g status")
  13.         }
  14.         mp.waitlock = lock
  15.         mp.waitunlockf = unlockf
  16.         gp.waitreason = reason
  17.         mp.waitTraceBlockReason = traceReason
  18.         mp.waitTraceSkip = traceskip
  19.         releasem(mp)
  20.         // can't do anything that might move the G between Ms here.
  21.         mcall(park_m) // 切换到了 g0栈,前面讲过mcall
  22. }
  23. // park continuation on g0.
  24. func park_m(gp *g) {
  25.         mp := getg().m
  26.         // 中间还有代码
  27.         if fn := mp.waitunlockf; fn != nil {
  28.                 ok := fn(gp, mp.waitlock)
  29.                 mp.waitunlockf = nil
  30.                 mp.waitlock = nil
  31.                 if !ok {
  32.                         if traceEnabled() {
  33.                                 traceGoUnpark(gp, 2)
  34.                         }
  35.                         casgstatus(gp, _Gwaiting, _Grunnable)
  36.                         execute(gp, true) // Schedule it back, never returns.
  37.                 }
  38.         }
  39.       schedule() //调了这个方法,之前讲过,一旦调用这个方法,就会给m找新的g
  40. }
复制代码
有个问题:gapark是小写的,程序员在编码中是使用不了的,那怎么让业务主动调用?
系统runtime里面很多方法有去调用,例如 time.Sleep、channel的等待等
系统调用完成时

在进行一些系统调用后,例如网络请求等会主动去调这个 exitsyscall() 这个方法,这个方法也会最终走到 schedule()
这个源码在 syscall_aix.go中,因为有好几层函数调用,就不贴出来了。
标记抢占 基于 morestack
  1. 1. 系统监控到 Goroutine 运行超过 10ms
  2. 2. 将 g.stackguard0 置为 Oxfffffade  
复制代码
morestack() 方法,在函数跳转时候,会自动调用,本意是检测下新的函数,有没有足够的栈空间。
系统在这个函数中,去做了一部分协程切换的,防止一些 耗时比较久的协程,不去触发上面两种方案。
看下源码:
  1. // Called during function prolog when more stack is needed.
  2. // record an argument size. For that purpose, it has no arguments.
  3. TEXT runtime·morestack(SB),NOSPLIT,$0-0
  4.   // 中间很多扩充栈空间的代码
  5.     CALL        runtime·newstack(SB) // 跟进这个方法
  6.         CALL        runtime·abort(SB)        // crash if newstack returns
  7.         RET
  8. // Goroutine preemption request.
  9. // 0xfffffade in hex.
  10. stackPreempt = uintptrMask & -1314
  11. func newstack() {
  12. // 中间还有很多源码
  13.   // 抢占标记   如果g的.stackguard0 字段被标记为抢占,就会触发下面的逻辑
  14.   stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
  15.   preempt := stackguard0 == stackPreempt
  16.   if preempt {
  17.         // Act like goroutine called runtime.Gosched.
  18.         gopreempt_m(gp) // never return
  19.       }
  20. }
  21. func gopreempt_m(gp *g) {
  22.           if traceEnabled() {
  23.                   traceGoPreempt()
  24.           }
  25.           goschedImpl(gp)
  26.   }
  27.   
  28. func goschedImpl(gp *g) {
  29.         //删了一些源码,最终调了 schedule
  30.         schedule()
  31. }
复制代码
基于信号的抢占标记
  1. 开头那个 for 循环的例子,虽然很无聊,但是去避开了上面那种方案,
  2. 1. 不会调用 gopark
  3. 2. 不会系统调用
  4. 3.不会调用 morestack,因为没有函数调用
复制代码
这时候,可以使用 信号 来触发协程的切换。
信号量可以在多线程和多进程直接进行通信(管道、共享内存、信号、消息队列一般作为多进程通信方式)。
原理:
  1. 操作系统中,有很多基于信号的底层通信方式,例如: SIGPIPE / SIGURG / SIGHUP
  2. 线程可以注册对应信号的处理函数
复制代码
go的实现流程:
  1. 注册 `SIGURG`信号的处理函数
  2. `GC`工作时,向目标线程发送信号
  3. 线程收到信号,触发调度,`gc`发送 `sigurg` 触发`runtime`的 `doSigPreempt()`
复制代码
能够猜到  doSigPreempt(), 最终会去调 schedule() 方法。 源码就不贴了。
开发中,协程过多的问题
  1.   1. 文件打开数限制
  2.       过多协程调用文件读写,会操作系统崩溃。
  3.   2. 内存限制
  4.       过多协程创建,达到了内存的限制  
  5.   
  6.   3. 调度开销过大
  7.       过多协程,导致调度器调度复杂度增大
复制代码
解决办法:
  1.     1. 优化业务逻辑
  2.     2. 利用 channel 的缓存区
  3.     3. 协程池
  4.     4. 调整系统资源
复制代码
2和3都是从控制协程的数量入手,2适合 单个业务场景,3适合全局。
1和4好理解
利用 channel 的缓存区

[code]func do(c chan interface{}) {        fmt.Println("do it")
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

水军大提督

金牌会员
这个人很懒什么都没写!

标签云

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