返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

3.4.1 流程

发布于 2024-10-12 19:16:04 字数 7411 浏览 0 评论 0 收藏 0

设置好标记,一旦解除 STW,调度核心(schedule)会立即检测到垃圾回收被启动。

随后,查找 P 绑定的工人(worker goroutine),执行标记工作。

1. start : gcBgMarkStartWorkers, stop!, startCycle, Blacken = 1, start!。

2. schedule : findrunnable --> G gcBgMarkWorker --> execute。

3. worker : gcBgMarkWorker --> gcDrain --> gcMarkDone (stop!, Blacken = 0)

--> schedEnableUser

--> endCycle

--> gcMarkTermination (start!)。

// proc.go

func schedule() {
    
    if gp == nil && gcBlackenEnabled != 0 {
        gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())
    }
    
    if gp == nil {
        gp, inheritTime = findrunnable()  // blocks until work is available
    }
    
    execute(gp, inheritTime)
}

虽然每个 P 都绑定有工人,但却承担不同的角色。

依据参与和抢占方式,分为不同类别:

  • gcMarkWorkerDedicatedMode : 正式工。专注标记工作,无法抢占。
  • gcMarkWorkerFractionalMode : 小时工。按需参加一定时长的工作,可被抢占。
  • gcMarkWorkerIdleMode : 临时工。没其他任务,临时参加标记工作,可被抢占。

各工种数量由 startCycle 方法,按利用率动态计算。

回收 CPU 利用率预期是 25%,以正式工为主,辅以临时工和小时工。

当利用率达不到预期时,会启动小时工参与。

临时工仅在找不到可执行任务时参与。

// mgc.go

const gcBackgroundUtilization = 0.25

func (c *gcControllerState) startCycle() {
    
    totalUtilizationGoal := float64(gomaxprocs) * gcBackgroundUtilization
    c.dedicatedMarkWorkersNeeded = int64(totalUtilizationGoal + 0.5)
    
    ...
}

findRunnableGCWorker, findrunnable

调度器按需激活不同类别的工人,返回(find)并使其进入(execute)工作状态。

// mgc.go

func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {

    // 检查是否可以启用。
    if !gcMarkWorkAvailable(_p_) {
        return nil
    }
    
    // 按需设置工作模式。
    if decIfPositive(&c.dedicatedMarkWorkersNeeded) {
        _p_.gcMarkWorkerMode = gcMarkWorkerDedicatedMode
    } else if c.fractionalUtilizationGoal == 0 {
        return nil
    } else {
        ...
        _p_.gcMarkWorkerMode = gcMarkWorkerFractionalMode
    }
    
    gp := _p_.gcBgMarkWorker.ptr()
    casgstatus(gp, _Gwaiting, _Grunnable)
    
    return gp
}

检查条件包括本地队列、全局队列,以及根对象扫描计数确定是否可以启用。

// mgc.go

func gcMarkWorkAvailable(p *p) bool {
    
    if p != nil && !p.gcw.empty() {
        return true
    }
    
    if !work.full.empty() {
        return true                                    // global work available
    }
    
    if work.markrootNext < work.markrootJobs {
        return true                                    // root scan work available
    }
    
    return false
}

另一函数 findrunnable,让找不到任务的 P 以临时工身份参与标记。

// proc.go

func findrunnable() (gp *g, inheritTime bool) {
    
    ...
    
    if gcBlackenEnabled != 0 && _p_.gcBgMarkWorker != 0 && gcMarkWorkAvailable(_p_) {
        
        _p_.gcMarkWorkerMode = gcMarkWorkerIdleMode
        gp := _p_.gcBgMarkWorker.ptr()
        casgstatus(gp, _Gwaiting, _Grunnable)
        
        return gp, false
    }
}

gcBgMarkWorker

工人的工作过程,贯穿 gcBgMarkWorker、gcDrain、gcMarkDone 函数。

工人从根对象(root)开始标记,将灰色对象添加到 P 本地或全局队列中。然后处理本地和全局队列。工人可能因抢占调度、其他任务,或者找不到任务(本地和全局队列为空,不包括其他 P 本地队列)等原因退出。

退出前,补上自己所消耗名额(work.nwait)。如此,在结束前,调度器会让其他该类型工人,继续完成工作。直到最后一位工人因找不到任务而退出时,才整体转入结束阶段。

// mgc.go

func gcBgMarkWorker(_p_ *p) {

    ...
    
    for {
        // 休眠,等待唤醒执行。
        gopark(...)
        
        // 占用名额,递减等待人数。
        decnwait := atomic.Xadd(&work.nwait, -1)
        
        // 开始标记工作。
        systemstack(func() {
            switch _p_.gcMarkWorkerMode {
            case gcMarkWorkerDedicatedMode:
                gcDrain(&_p_.gcw, gcDrainFlushBgCredit)
            case gcMarkWorkerFractionalMode:
                gcDrain(&_p_.gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit)
            case gcMarkWorkerIdleMode:
                gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
            }
        })
        
        // 退出前(比如完成任务或被抢占)补上占用的名额。
        incnwait := atomic.Xadd(&work.nwait, +1)
        
        // 如果等待人数齐员,且全局队列没有可用任务,表示标记任务结束。
        if incnwait == work.nproc && !gcMarkWorkAvailable(nil) {
            gcMarkDone()
        }
    }
}

gcDrainUntilPreempt: 抢占时退出。

gcDrainIdle: 有其他任务时退出。

gcDrainFractional: 利用率超过阈值(20%)时退出。

gcDrainFlushBgCredit: 保存信用值。

名额设置很有意思。假定有很多工人,反正只要两个能增减平衡即可。

// mgc.go

func gcBgMarkPrepare() {
    
    // Background marking will stop when the work queues are empty
    // and there are no more workers (note that, since this is
    // concurrent, this may be a transient state, but mark
    // termination will clean it up). Between background workers
    // and assists, we don't really know how many workers there
    // will be, so we pretend to have an arbitrarily large number
    // of workers, almost all of which are "waiting". While a
    // worker is working it decrements nwait. If nproc == nwait,
    // there are no workers.
    
    work.nproc = ^uint32(0)
    work.nwait = ^uint32(0)
}

gcMarkDone

由于工人会中途退出(exit gcDrain),有必要检查所有 P 本地队列是否有遗留任务。

遗留任务必须上交到全局队列,以便调度器启动新工人完成收尾任务。

// mgc.go

func gcMarkDone() {
top:
    
    // 重新检查,如果任务未完成,则停止后续操作。
    if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil)) {
        return
    }
    
    // 确保所有 P 上交任务。
    gcMarkDoneFlushed = 0
    systemstack(func() {
        
        forEachP(func(_p_ *p) {
            // 上交任务。
            _p_.gcw.dispose()
            if _p_.gcw.flushedWork {
                atomic.Xadd(&gcMarkDoneFlushed, 1)
                _p_.gcw.flushedWork = false
            }
        })
        
        casgstatus(gp, _Gwaiting, _Grunning)
    })
    
    // 有遗留任务上交,重新检查,避免提前终止。
    if gcMarkDoneFlushed != 0 {
        goto top
    }
    
    // 转入标记结束阶段 。。。。。。
    
    // STW!
    systemstack(stopTheWorldWithSema)
    
    // 禁用工人和辅助。
    atomic.Store(&gcBlackenEnabled, 0)
    
    // 唤醒被休眠的辅助。
    gcWakeAllAssists()
    
    // 重新开始用户逻辑。
    schedEnableUser(true)
    
    // 结束控制器周期,计算下次触发比率。
    nextTriggerRatio := gcController.endCycle()
    
    // 标记终止!解除 STW!
    gcMarkTermination(nextTriggerRatio)
}

gcMarkTermination

标记结束时,设置相关状态,通知调度器停止相关工作。

随后完成一些剩余工作,开启并发清理。

调度器会检查到该状态(gcBlackenEnabled = 0),从而不再参与回收工作。

// mgc.go

func gcMarkTermination(nextTriggerRatio float64) {
    
    // 重置相关状态。
    atomic.Store(&gcBlackenEnabled, 0)
    setGCPhase(_GCmarktermination)
    
    // 收尾检查和统计。
    systemstack(func() {
        gcMark(startTime)
    })
    
    // 开启清理。(禁用写屏障)
    systemstack(func() {
        setGCPhase(_GCoff)
        gcSweep(work.mode)
    })
    
    // 更新回收阈值等。
    gcSetTriggerRatio(nextTriggerRatio)
    
    // 解除 STW !
    systemstack(func() { startTheWorldWithSema(true) })
    
    // 释放未使用的 stack 块。
    systemstack(freeStackSpans)
    
    // 非并发清理模式,当前 goroutine 将暂停。
    if !concurrentSweep {
         Gosched()
    }
}
func gcMark(start_time int64) {
 
    work.tstart = start_time
    
    // 确保没有任务遗漏,否则抛出异常 ...
    cachestats()
    
    memstats.heap_marked = work.bytesMarked
    memstats.heap_live = work.bytesMarked
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文