Go垃圾回收[3]-并行标记执行模式

420次阅读  |  发布于2年以前

前情回顾

标记准备阶段切换到后台标记协程

标记准备阶段的第二个问题是如何切换到后台标记协程执行。在标记准备阶段执行了STW、在STW阶短暂的暂停了所有的协程。可以预料到,当关闭STW准备再次启动所有的协程时,每一个逻辑处理器P会进入一轮新的调度循环,在调度循环的最开始的一步会判断是否处于GC阶段,如果是,尝试判断当前P 是否需要执行后台标记任务。

func schedule() {
//  正在 GC,去找 GC 的 g
    if gp == nil && gcBlackenEnabled != 0 {
        gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())
        tryWakeP = tryWakeP || gp != nil
    }
}

如果代表了执行完整的后台标记协程的字段dedicatedMarkWorkersNeeded大于0,则直接执行后台标记任务。否则,如果协助协程字段fractionalUtilizationGoal大于0,并且当前P执行标记任务的时间 小于 fractionalUtilizationGoal*当前标记周期总时间,仍然会执行后台标记任务,但是并不会在整个标记周期内一直执行。本小节下面会看到,这对应着后台标记协程的不同执行模式。

if decIfPositive(&c.dedicatedMarkWorkersNeeded) {
        _p_.gcMarkWorkerMode = gcMarkWorkerDedicatedMode
    } else if c.fractionalUtilizationGoal == 0 {
        return nil
    } else {
        delta := nanotime() - gcController.markStartTime
        if delta > 0 && float64(_p_.gcFractionalMarkTime)/float64(delta) > c.fractionalUtilizationGoal {
            return nil
        }
        _p_.gcMarkWorkerMode = gcMarkWorkerFractionalMode
    }

并行标记执行阶段

在并发标记执行阶段,后台标记协程可以与执行用户代码的协程并行执行。Go语言的目标是后台标记协程暂用CPU的时间为25%,最大限度不因为执行GC而中断或减慢用户协程的执行。后台标记协程有3种不同的模式:

DedicatedMode代表处理器专门负责标记对象,不会被调度器抢占;

FractionalMode代表协助后台标记,其在整个标记阶段只会花费一定部分时间执行,因此,在标记阶段当完成时间的目标后,会自动退出。

IdleMode 为当处理器没有查找到可以执行的 协程时,执行垃圾收集的标记任务直到被抢占。标记阶段的核心逻辑位于gcDrain 函数,第二个参数为flag位,大部分flag和后台标记协程的3种不同的模式有关。

func gcDrain(gcw *gcWork, flags gcDrainFlags)

flag有4种,用于指定后台标记协程的不同行为。gcDrainUntilPreempt 为当 Goroutine 的 preempt 字段被设置成 true 时返回, 代表当前后台标记可以被抢占。gcDrainFlushBgCredit计算后台完成的标记任务量以减少并行标记期间用户程序执行辅助垃圾收集的工作量,后面还会详细介绍。gcDrainIdle对应IdleMode模式, 当处理器上包含其他待执行 协程 时退出.gcDrainFractional 对应IdleMode模式,当完成目标时间后退出。

由于在DedicatedMode模式下,将会一直执行后台标记任务,这意味着当前P本地队列中的协程将一直得不到执行,这是不能接受的。所以Go语言中的做法是首先执行可以被抢占的后台标记任务,如果发现被其他协程抢占了,当前的P并不会执行其他协程。而是会选择将其他协程转移到全局队列中,并取消gcDrainUntilPreempt标志,开始执行不能够被抢占的模式。

case gcMarkWorkerDedicatedMode:
                gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
                if gp.preempt {
                    lock(&sched.lock)
                    for {
                        gp, _ := runqget(_p_)
                        if gp == nil {
                            break
                        }
                        globrunqput(gp)
                    }
                    unlock(&sched.lock)
                }
                gcDrain(&_p_.gcw, gcDrainFlushBgCredit)
            case gcMarkWorkerFractionalMode:
                gcDrain(&_p_.gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit)
            case gcMarkWorkerIdleMode:
                gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
            }

对于FractionalMode模式和IdleMode模式,都允许被抢占。除此之外,FractionalMode模式加上了gcDrainFractional标志表明当前协程会在到达目标时间后退出,IdleMode模式加上了gcDrainIdle标志表明会在发现有其他协程可以运行时退出。最后三种模式都加上了gcDrainFlushBgCredit标志,用于计算后台完成的标记任务量,并唤醒之前由于分配内存太频繁而陷入等待的用户协程(关于辅助标记,将在后面介绍)。

总结

并发标记阶段有多种模式,这些模式的目的是为了后台标记协程占用25%的CPU时间,协调好后台标记协程与用户协程的关系,这只是比较宏观的角度来讨论了并发标记阶段。并发标记阶段的故事后面还更加精彩.....

各位看官,下周再见。see you~

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8