Runtime: 当一个goroutine 运行结束后会发生什么

上一篇我们介绍了当创建一个goroutine 会经历些什么,今天我们再看下当一个goroutine 运行结束的时候,又会发生什么?

go version 1.15.6。

主要源文件为 src/runtime/proc.go

当一个goroutine 运行结束的时候,默认会执行一个 goexit1() 的函数,这是一个只有八行代码的函数,其中最后以通过 mcall() 调用 goexit0 函数结束。因此我们主要关注 goexit0 函数即可。这里为了更好让大家理解,以此之前需要先介绍一下 mcall() 函数。

stubs.go 源文件中只是对 mcall() 函数进行了声明,并无函数体,说明这个函数是使用汇编实现的。但这里我们并不需要关心它的具体实现,只要知道它的主要工作职责就可以了,所有信息我们都可以通过函数注释得知。

// mcall switches from the g to the g0 stack and invokes fn(g),
// where g is the goroutine that made the call.

// mcall函数用来从 g 切换到 g0 栈并调用gn(g)函数,这里的 g 指发起调用的那个goroutine。

// mcall saves g's current PC/SP in g->sched so that it can be restored later.
// It is up to fn to arrange for that later execution, typically by recording
// g in a data structure, causing something to call ready(g) later.

// mcall 会将当前 g 的 PC/SP 信息到 g->sched,以便以后恢复执行
// 由 fn 函数来安排稍后的执行,一般是通过将g记录到一个数据结构中,以后再通过调用 ready(g) 进行唤醒

// mcall returns to the original goroutine g later, when g has been rescheduled.
// fn must not return at all; typically it ends by calling schedule, to let the m
// run other goroutines.

// 当 g 被重新调度后,mcall将返回原来的 g
// fn 不能返回,通常是在它调度结束后,直接让 m 运行其它的 goroutine

//
// mcall can only be called from g stacks (not g0, not gsignal).

// mcall 只能从 g 栈中发起调用,不能是 g0 或 gsignal

//
// This must NOT be go:noescape: if fn is a stack-allocated closure,
// fn puts g on a run queue, and g executes before fn returns, the
// closure will be invalidated while it is still executing.

// fn 必须是 go:noescape(非逃逸函数)
// 如果 fn 是堆栈分配的一个闭包函数, 则 fn 函数将 g 放在一个runqueue 中,g并在fn返回之前执行,当仍在执行期间闭包将失效

func mcall(fn func(*g))

从上面的注释可看出来,mcall() 函数主要有来实现从 gg0 的切换,对特殊的g0的切换过程,我们在 g0 特殊的goroutine 文章里已经讲过,每次 g 之间的切换都需要经过g0

在切换 g 之前,需要记录当前 g 的上下文信息(PC/SP)保存到调度器(g->sched),以便下次恢复运行时使用,所有这些是切换一个 g 的必要前提条件。

你可能会想 当一个g 全部执行结束后,将 g 的上下文信息保存到数据结构中好像意义不大,毕竟当前 goroutine 会被直接销毁,永远也不可能被再次调度执行。其实在golang中,当一个goroutine执行完并不是直接释放掉,为了以便后期可以直接复用(复用逻辑见 Runtime: 创建一个goroutine都经历了什么?
,避免重新创建goroutine而带来的开销成本。一般会将使用完的 g 进行清理后,将其保存到一个gfree 列表中。

好了,现在我们再看看 goexit0 函数都执行了哪些操作。

// goexit continuation on g0.
func goexit0(gp *g) {}

这时 goexit0 是运行在 g0 上的,原因就是它是由 mcall() 函数发起调用的,上方注释已经说明了这一点。

func goexit0(gp *g) {
    // 这里的 _g_ 是g0
    _g_ := getg()

    // 状态切换
    casgstatus(gp, _Grunning, _Gdead)

    // 系统goroutines 数据减少1
    if isSystemGoroutine(gp, false) {
        atomic.Xadd(&sched.ngsys, -1)
    }

    // 释放相关资源,比较简单
    gp.m = nil
    locked := gp.lockedm != 0
    gp.lockedm = 0
    _g_.m.lockedg = 0
    gp.preemptStop = false
    gp.paniconfault = false
    gp._defer = nil // should be true already but just in case.
    gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
    gp.writebuf = nil
    gp.waitreason = 0
    gp.param = nil
    gp.labels = nil
    gp.timer = nil

    if gcBlackenEnabled != 0 && gp.gcAssistBytes > 0 {
        // Flush assist credit to the global pool. This gives
        // better information to pacing if the application is
        // rapidly creating an exiting goroutines.
        scanCredit := int64(gcController.assistWorkPerByte * float64(gp.gcAssistBytes))
        atomic.Xaddint64(&gcController.bgScanCredit, scanCredit)
        gp.gcAssistBytes = 0
    }

    // 删除 m 与当前gourtine m->curg 的关联
    dropg()

    if GOARCH == "wasm" { // no threads yet on wasm
        gfput(_g_.m.p.ptr(), gp)
        schedule() // never returns
    }

    if _g_.m.lockedInt != 0 {
        print("invalid m->lockedInt = ", _g_.m.lockedInt, "n")
        throw("internal lockOSThread error")
    }

    // 将g放入当前 P 的 gFree 列表,如果列表太长(>=64),则转移一半的g到全局列表 sched.gFree
    gfput(_g_.m.p.ptr(), gp)

    if locked {
        // The goroutine may have locked this thread because
        // it put it in an unusual kernel state. Kill it
        // rather than returning it to the thread pool.

        // Return to mstart, which will release the P and exit
        // the thread.
        if GOOS != "plan9" { // See golang.org/issue/22227.
            gogo(&_g_.m.g0.sched)
        } else {
            // Clear lockedExt on plan9 since we may end up re-using
            // this thread.
            _g_.m.lockedExt = 0
        }
    }

    // 调度
    schedule()
}

对于 dropg() 函数,这里不再介绍,有兴趣的可以看一下实现源码。

这里介绍一下放入g的 gfput() 函数,看看是如何存放的。

// Put on gfree list.
// If local list is too long, transfer a batch to the global list.
func gfput(_p_ *p, gp *g) {
    if readgstatus(gp) != _Gdead {
        throw("gfput: bad status (not Gdead)")
    }

    stksize := gp.stack.hi - gp.stack.lo

    if stksize != _FixedStack {
        // non-standard stack size - free it.
        stackfree(gp.stack)
        gp.stack.lo = 0
        gp.stack.hi = 0
        gp.stackguard0 = 0
    }

    _p_.gFree.push(gp)
    _p_.gFree.n++
    if _p_.gFree.n >= 64 {
        lock(&sched.gFree.lock)
        for _p_.gFree.n >= 32 {
            _p_.gFree.n--
            gp = _p_.gFree.pop()
            if gp.stack.lo == 0 {
                sched.gFree.noStack.push(gp)
            } else {
                sched.gFree.stack.push(gp)
            }
            sched.gFree.n++
        }
        unlock(&sched.gFree.lock)
    }
}

如果将 g 放入当前 PgFree 列表中后,发现当前P的 gFree 存放的g数量 >= 64,则需要转移一批 g 到全局列表(sched.gFree)中,基本是转移走 1/2g,直到只剩下31个为止。之所以批量转移也是为了性能考虑。全局调度器上sched.gFreestacknoStack 的区域)

其实在runtime 源码里有太多与计算相关的算法不是原来大小的两倍就是1/2倍,如map的扩容是两倍或者1/2倍,切片扩容在<1024的情况下也是两倍的扩容,大于或等于1024的情况下是1.25倍,除此之外还有栈的扩容和收缩

总结

当一个goroutine结果后会执行以下操作

  1. 调用 mcall() 函数,将当前 g 切换到 g0,并保存当前 g 的上下文信息(PC/SP)到调度器
  2. g0 中切换当前 goroutine 的运行状态 _Grunning => _Gdead
  3. 释放当前 goroutine 相关的资源及与其绑定的m信息
  4. 将当前空闲的goroutine放在 PgFree 列表中,以便复用。如果列表太长,同转移一半的g到全部列表 sched.gFeee 中,转移前要加锁
  5. 再次调度

本文出自:https://blog.haohtml.com/archives/23437

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容