我们通常创建一个协程并运行一段逻辑。 代码如下所示。
portant;border-style: initial !important;border-color: initial !important;">package main
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">import (
portant;border-style: initial !important;border-color: initial !important;"> "fmt"
portant;border-style: initial !important;border-color: initial !important;"> "time"
portant;border-style: initial !important;border-color: initial !important;">)
portant;border-style: initial !important;border-color: initial !important;">func Foo() {
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印1")
portant;border-style: initial !important;border-color: initial !important;"> defer fmt.Println("打印2")
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印3")
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">func main() {
portant;border-style: initial !important;border-color: initial !important;"> go Foo()
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印4")
portant;border-style: initial !important;border-color: initial !important;"> time.Sleep(1000*time.Second)
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">// 这段代码,正常运行会有下面的结果
portant;border-style: initial !important;border-color: initial !important;">打印4
portant;border-style: initial !important;border-color: initial !important;">打印1
portant;border-style: initial !important;border-color: initial !important;">打印3
portant;border-style: initial !important;border-color: initial !important;">打印2
请注意,上面的“print 2”处于延迟状态,因此它将在函数结束之前打印。 因此它被放置在“print 3”之后。
那么今天的问题是,如何让 Foo() 函数执行到一半,比如打印 2 时,就会退出协程。输出如下结果
portant;border-style: initial !important;border-color: initial !important;">打印4
portant;border-style: initial !important;border-color: initial !important;">打印1
portant;border-style: initial !important;border-color: initial !important;">打印2
不用太做作,我会直接给你答案。
在“print 2”后面插入一个.(),协程将直接结束。 并且在结束之前,仍然可以在defer中执行print 2。
portant;border-style: initial !important;border-color: initial !important;">package main
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">import (
portant;border-style: initial !important;border-color: initial !important;"> "fmt"
portant;border-style: initial !important;border-color: initial !important;"> "runtime"
portant;border-style: initial !important;border-color: initial !important;"> "time"
portant;border-style: initial !important;border-color: initial !important;">)
portant;border-style: initial !important;border-color: initial !important;">func Foo() {
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印1")
portant;border-style: initial !important;border-color: initial !important;"> defer fmt.Println("打印2")
portant;border-style: initial !important;border-color: initial !important;"> runtime.Goexit() // 加入这行
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印3")
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">func main() {
portant;border-style: initial !important;border-color: initial !important;"> go Foo()
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印4")
portant;border-style: initial !important;border-color: initial !important;"> time.Sleep(1000*time.Second)
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">// 输出结果
portant;border-style: initial !important;border-color: initial !important;">打印4
portant;border-style: initial !important;border-color: initial !important;">打印1
portant;border-style: initial !important;border-color: initial !important;">打印2
可以看到行打印3并没有出现,协程确实提前结束了。
其实面试题就到这里了。 这波自问自答还好吗?
但这不是今天的重点,我们需要弄清楚其中的内在逻辑。
什么是 。()?
看一下内部实现。
portant;border-style: initial !important;border-color: initial !important;">func Goexit() {
portant;border-style: initial !important;border-color: initial !important;"> // 以下函数省略一些逻辑...
portant;border-style: initial !important;border-color: initial !important;"> gp := getg()
portant;border-style: initial !important;border-color: initial !important;"> for {
portant;border-style: initial !important;border-color: initial !important;"> // 获取defer并执行
portant;border-style: initial !important;border-color: initial !important;"> d := gp._defer
portant;border-style: initial !important;border-color: initial !important;"> reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
portant;border-style: initial !important;border-color: initial !important;"> }
portant;border-style: initial !important;border-color: initial !important;"> goexit1()
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">func goexit1() {
portant;border-style: initial !important;border-color: initial !important;"> mcall(goexit0)
portant;border-style: initial !important;border-color: initial !important;">}
从代码上看,.()会先执行defer中的方法。 这就解释了为什么开头代码中的defer中的print 2可以正常输出。
然后再次执行代码。 本质就是恰到好处的简单封装。
我们可以继续跟踪代码,看看做了什么。
portant;border-style: initial !important;border-color: initial !important;">// goexit continuation on g0.
portant;border-style: initial !important;border-color: initial !important;">func goexit0(gp *g) {
portant;border-style: initial !important;border-color: initial !important;"> // 获取当前的 goroutine
portant;border-style: initial !important;border-color: initial !important;"> _g_ := getg()
portant;border-style: initial !important;border-color: initial !important;"> // 将当前goroutine的状态置为 _Gdead
portant;border-style: initial !important;border-color: initial !important;"> casgstatus(gp, _Grunning, _Gdead)
portant;border-style: initial !important;border-color: initial !important;"> // 全局协程数减一
portant;border-style: initial !important;border-color: initial !important;"> if isSystemGoroutine(gp, false) {
portant;border-style: initial !important;border-color: initial !important;"> atomic.Xadd(&sched.ngsys, -1)
portant;border-style: initial !important;border-color: initial !important;"> }
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;"> // 省略各种清空逻辑...
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;"> // 把g从m上摘下来。
portant;border-style: initial !important;border-color: initial !important;"> dropg()
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;"> // 把这个g放回到p的本地协程队列里,放不下放全局协程队列。
portant;border-style: initial !important;border-color: initial !important;"> gfput(_g_.m.p.ptr(), gp)
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;"> // 重新调度,拿下一个可运行的协程出来跑
portant;border-style: initial !important;border-color: initial !important;"> schedule()
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
该代码具有较高的信息密度。
许多术语可能会令人困惑。
简单描述一下,Go语言中有一个GMP模型。 M是内核线程,G是我们平时使用的协程,P将是G和M之间的工具,负责调度G在M上运行。
GMP图
既然是调度,也就是说不是每个G都能一直运行的。 当G无法运行时,将其存储并安排下一个可以运行的G运行。
对于暂时无法运行的G,P上会有一个本地队列来存储这些G。 如果P的本地队列无法存储它们,就会有一个全局队列做类似的事情。
了解了这个背景之后,我们再回到方法上来。 我们所做的是将当前协程 G 设置为状态,然后将其从 M 中取出并尝试将其放回 P 的本地队列中。 然后重新安排一波,再拿一个能跑的G,拿出来跑。
所以简单总结一下,只要执行这个函数,当前协程就会退出,同时可以调度下一个可执行协程运行。
看到这里大家应该能明白为什么开头代码中的.()会让协程只执行一半就结束了。
指某东西的用途
我明白,但我忍不住想知道。 如果你在面试中问这个问题,只能说明你遇到了一个喜欢为难年轻人的面试官,但谁是认真的人,会跑完一半的协程就结束呢? 那么真正的用途是什么?
有一个小细节。 不知道大家调试的时候有没有注意过。
为了说明这个问题,这里有一段代码。
portant;border-style: initial !important;border-color: initial !important;">package main
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">import (
portant;border-style: initial !important;border-color: initial !important;"> "fmt"
portant;border-style: initial !important;border-color: initial !important;"> "time"
portant;border-style: initial !important;border-color: initial !important;">)
portant;border-style: initial !important;border-color: initial !important;">func Foo() {
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印1")
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">func main() {
portant;border-style: initial !important;border-color: initial !important;"> go Foo()
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印3")
portant;border-style: initial !important;border-color: initial !important;"> time.Sleep(1000*time.Second)
portant;border-style: initial !important;border-color: initial !important;">}
这是一个非常简单的代码,它输出什么并不重要。 通过 go 关键字开始执行 Foo(),并在打印后结束。 主协程休眠了很长一段时间,就等死了。
在这里,在我们新启动的协程中,我们可以在 Foo() 函数中设置一个断点。 然后调试一下。
你会发现这个协程的栈底是从.()开始的。
如果你留心的话,你会发现所有的栈底其实都是从这个函数开始的。 让我们继续看代码。
它是什么?
如果你点击上面的调试堆栈,你会发现这是一个汇编函数。 可以看到调用了包中的()函数。
portant;border-style: initial !important;border-color: initial !important;">// The top-most function running on a goroutine
portant;border-style: initial !important;border-color: initial !important;">// returns to goexit+PCQuantum.
portant;border-style: initial !important;border-color: initial !important;">TEXT runtime·goexit(SB),NOSPLIT,$0-0
portant;border-style: initial !important;border-color: initial !important;"> BYTE $0x90 // NOP
portant;border-style: initial !important;border-color: initial !important;"> CALL runtime·goexit1(SB) // does not return
portant;border-style: initial !important;border-color: initial !important;"> // traceback from goexit1 must hit code range of goexit
portant;border-style: initial !important;border-color: initial !important;"> BYTE $0x90 // NOP
所以我按照/proc.go中的代码进行操作。
portant;border-style: initial !important;border-color: initial !important;">// 省略部分代码
portant;border-style: initial !important;border-color: initial !important;">func goexit1() {
portant;border-style: initial !important;border-color: initial !important;"> mcall(goexit0)
portant;border-style: initial !important;border-color: initial !important;">}
听起来很熟悉? 这不是我们一开始就讲的吗? () 在内部执行。
为什么这个方法位于每个堆栈的底部?
首先我们要知道的是函数栈的执行过程是先进后出的。
假设我们有以下代码
portant;border-style: initial !important;border-color: initial !important;">func main() {
portant;border-style: initial !important;border-color: initial !important;"> B()
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">func B() {
portant;border-style: initial !important;border-color: initial !important;"> A()
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">func A() {
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">}
上面的代码是main运行函数B,然后函数B运行函数A。代码执行时,会像下面的动画一样。
函数栈执行顺序
这是一个先进后出的过程,也就是我们常说的函数栈。 子函数A()执行完后,会返回到父函数B()。 执行完B()后,最终会返回到main。 ()。 这里堆栈的底部是main()。 如果它被插入到堆栈的底部,那么当程序执行结束时它可以运行到它。
结合之前说过的,我们可以知道,此时,协程中的业务代码运行完毕后,就会执行栈底,从而实现协程的退出,并调度下一个可执行文件G运行。
那么问题又来了,栈底的插入是谁做的,什么时候做的?
为了直接给出答案,/proc.go中有一个方法。 每当创建协程时都会使用此方法。 有一个地方是这么说的。
portant;border-style: initial !important;border-color: initial !important;">func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {
portant;border-style: initial !important;border-color: initial !important;"> // 获取当前g
portant;border-style: initial !important;border-color: initial !important;"> _g_ := getg()
portant;border-style: initial !important;border-color: initial !important;"> // 获取当前g所在的p
portant;border-style: initial !important;border-color: initial !important;"> _p_ := _g_.m.p.ptr()
portant;border-style: initial !important;border-color: initial !important;"> // 创建一个新 goroutine
portant;border-style: initial !important;border-color: initial !important;"> newg := gfget(_p_)
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;"> // 底部插入goexit
portant;border-style: initial !important;border-color: initial !important;"> newg.sched.pc = funcPC(goexit) + sys.PCQuantum
portant;border-style: initial !important;border-color: initial !important;"> newg.sched.g = guintptr(unsafe.Pointer(newg))
portant;border-style: initial !important;border-color: initial !important;"> // 把新创建的g放到p中
portant;border-style: initial !important;border-color: initial !important;"> runqput(_p_, newg, true)
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;"> // ...
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
主要逻辑是获取当前协程G所在的调度器P,然后创建一个新的G并在栈底插入一个。
所以我们每次调试的时候,都可以在函数栈的底部看到一个函数。
main函数也是协程,那么栈底也是吗?
关于main函数栈底是否也有断点,我们看一下下面代码中的断点。 直接得到结果。
主函数栈的底部也是()。
从.s中可以看到Go程序启动的过程。 这里所说的其实就是.main。
portant;border-style: initial !important;border-color: initial !important;"> // create a new goroutine to start program
portant;border-style: initial !important;border-color: initial !important;"> MOVQ $runtime·mainPC(SB), AX // 也就是runtime.main
portant;border-style: initial !important;border-color: initial !important;"> PUSHQ AX
portant;border-style: initial !important;border-color: initial !important;"> PUSHQ $0 // arg size
portant;border-style: initial !important;border-color: initial !important;"> CALL runtime·newproc(SB)
通过创建 .main 协程,main.main 函数将在 .main 中启动。 这是我们平时写的main函数。
portant;border-style: initial !important;border-color: initial !important;">// runtime/proc.go
portant;border-style: initial !important;border-color: initial !important;">func main() {
portant;border-style: initial !important;border-color: initial !important;"> // 省略大量代码
portant;border-style: initial !important;border-color: initial !important;"> fn := main_main // 其实就是我们的main函数入口
portant;border-style: initial !important;border-color: initial !important;"> fn()
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">//go:linkname main_main main.main
portant;border-style: initial !important;border-color: initial !important;">func main_main()
结论是main函数实际上是由. 只要创建了,栈底就会有一个。
os.Exit() 和 .() 有什么区别
最后,我们回到开头的问题,体会一下开头和结尾的呼应。
对于一开始的面试题,除了.()之外,还可以用os.Exit()代替吗?
它们也有“退出”的意思,但退出的对象不同。 os.Exit() 指的是整个进程的退出; .() 表示协程退出。
可以想象,如果使用os.Exit()来代替,defer中的内容将不会被执行。
portant;border-style: initial !important;border-color: initial !important;">package main
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">import (
portant;border-style: initial !important;border-color: initial !important;"> "fmt"
portant;border-style: initial !important;border-color: initial !important;"> "os"
portant;border-style: initial !important;border-color: initial !important;"> "time"
portant;border-style: initial !important;border-color: initial !important;">)
portant;border-style: initial !important;border-color: initial !important;">func Foo() {
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印1")
portant;border-style: initial !important;border-color: initial !important;"> defer fmt.Println("打印2")
portant;border-style: initial !important;border-color: initial !important;"> os.Exit(0)
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印3")
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">func main() {
portant;border-style: initial !important;border-color: initial !important;"> go Foo()
portant;border-style: initial !important;border-color: initial !important;"> fmt.Println("打印4")
portant;border-style: initial !important;border-color: initial !important;"> time.Sleep(1000*time.Second)
portant;border-style: initial !important;border-color: initial !important;">}
portant;border-style: initial !important;border-color: initial !important;">
portant;border-style: initial !important;border-color: initial !important;">// 输出结果
portant;border-style: initial !important;border-color: initial !important;">打印4
portant;border-style: initial !important;border-color: initial !important;">打印1
portant;border-style: initial !important;border-color: initial !important;">
总结
•通过.()可以提前结束协程,并在结束之前执行defer的内容。 •.() 实际上是正确的封装。 只要执行这个函数,当前协程就会退出,同时可以调度下一个协程。 一个可执行的协程出来并运行。 • 可以通过在函数堆栈底部插入一个来创建新函数。 •os.Exit()指的是整个进程的退出; .() 表示协程退出。 两者之间的含义是有区别的。
终于
无用的知识增加了。
一般情况下,在业务开发中,谁会毫无困难地执行这个功能呢?
但如果你开发时不关心,并不代表面试官不关心!
下次面试官问你,如果执行到一半想退出协程怎么办? 你知道如何回答吗?