首页 > 资讯 > 科技 > 正文
2024-02-18 22:05

Go如何让协程中途退出?

我们通常创建一个协程并运行一段逻辑。 代码如下所示。

portant;border-style: initial !important;border-color: initial !important;">package mainportant;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;">打印4portant;border-style: initial !important;border-color: initial !important;">打印1portant;border-style: initial !important;border-color: initial !important;">打印3portant;border-style: initial !important;border-color: initial !important;">打印2

请注意,上面的“print 2”处于延迟状态,因此它将在函数结束之前打印。 因此它被放置在“print 3”之后。

那么今天的问题是,如何让 Foo() 函数执行到一半,比如打印 2 时,就会退出协程。输出如下结果

portant;border-style: initial !important;border-color: initial !important;">打印4portant;border-style: initial !important;border-color: initial !important;">打印1portant;border-style: initial !important;border-color: initial !important;">打印2

不用太做作,我会直接给你答案。

在“print 2”后面插入一个.(),协程将直接结束。 并且在结束之前,仍然可以在defer中执行print 2。

portant;border-style: initial !important;border-color: initial !important;">package mainportant;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;">打印4portant;border-style: initial !important;border-color: initial !important;">打印1portant;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._deferportant;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;">  // 获取当前的 goroutineportant;border-style: initial !important;border-color: initial !important;">    _g_ := getg()portant;border-style: initial !important;border-color: initial !important;">    // 将当前goroutine的状态置为 _Gdeadportant;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 mainportant;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 goroutineportant;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-0portant;border-style: initial !important;border-color: initial !important;">    BYTE    $0x90    // NOPportant;border-style: initial !important;border-color: initial !important;">    CALL    runtime·goexit1(SB)    // does not returnportant;border-style: initial !important;border-color: initial !important;">    // traceback from goexit1 must hit code range of goexitportant;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;">    // 获取当前gportant;border-style: initial !important;border-color: initial !important;">  _g_ := getg()portant;border-style: initial !important;border-color: initial !important;">    // 获取当前g所在的pportant;border-style: initial !important;border-color: initial !important;">    _p_ := _g_.m.p.ptr()portant;border-style: initial !important;border-color: initial !important;">  // 创建一个新 goroutineportant;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;">    // 底部插入goexitportant;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 programportant;border-style: initial !important;border-color: initial !important;">    MOVQ    $runtime·mainPC(SB), AX        // 也就是runtime.mainportant;border-style: initial !important;border-color: initial !important;">    PUSHQ    AXportant;border-style: initial !important;border-color: initial !important;">    PUSHQ    $0            // arg sizeportant;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.goportant;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.mainportant;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 mainportant;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;">打印4portant;border-style: initial !important;border-color: initial !important;">打印1portant;border-style: initial !important;border-color: initial !important;"> 

总结

•通过.()可以提前结束协程,并在结束之前执行defer的内容。 •.() 实际上是正确的封装。 只要执行这个函数,当前协程就会退出,同时可以调度下一个协程。 一个可执行的协程出来并运行。 • 可以通过在函数堆栈底部插入一个来创建新函数。 •os.Exit()指的是整个进程的退出; .() 表示协程退出。 两者之间的含义是有区别的。

终于

无用的知识增加了。

一般情况下,在业务开发中,谁会毫无困难地执行这个功能呢?

但如果你开发时不关心,并不代表面试官不关心!

下次面试官问你,如果执行到一半想退出协程怎么办? 你知道如何回答吗?