永远不要在不知道如何停止的情况下启动一个 goroutine


  1. 背景
  2. 协程阻塞
    1. context
    2. channel & select
  3. 协程退出
  4. 完美退出

背景

在 Go 中,goroutine 的创建成本低,调度效率高,同时存在数十万个 goroutine 并不奇怪。虽然单个 goroutine 使用的内存有限,但是不意味着可以毫无限制的创建 goroutine

Never start a goroutine without knowing how it will stop

每次启动 goroutine 时,必须知道 goroutine 何时、如何退出。否则,程序就潜藏着内存泄漏问题。在讨论协程退出前,先了解下协程为何阻塞

协程阻塞

协程阻塞无法自由退出,主要因为以下两点:

  • 超时控制
  • 流程控制

context

前者,很容易理解。一般来说启动 goroutine 处理事务,对于事务的处理完成时间都有一定的预期 举例:

  • RPC调用:最大超时时间不会超过用户的等待时间
  • 定时任务:执行一次的时间不应该超过启动的间隔

针对何时退出,Go 中 提供了 Context 用于 goroutine 生命周期管理

  • Cancellation via context.WithCancel.
  • Timeout via context.WithDeadline.
    req, err := http.NewRequest("GET", "https://play.golang.org/", nil)
    if err != nil {
    	log.Fatalf("%v", err)
    }
    
    ctx, cancel := context.WithTimeout(req.Context(), 1*time.Second)
    defer cancel()
    
    req = req.WithContext(ctx)
    client := http.DefaultClient
    resp, err := client.Do(req)
    if err != nil {
    	log.Fatalf("%v", err)
    }
    fmt.Printf("%v\n", resp.StatusCode)

channel & select

后者,相对来说比较难理解一些。尤其是其他语言的使用者,对于他们而言,程序中的流程控制一般意味着:

  • if/else
  • for loop

在 Go 中,类似的理解仅仅对了一小半。因为 channel 和 select 才是流程控制的重点

channel 提供了强大能力,帮助数据从一个 goroutine 流转到另一个 goroutine。也意味着,channel 对程序的 数据流控制流 同时存在影响。

  • closed 的 channel 永远不会阻塞
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan bool, 2)
        ch <- true
        ch <- true
        close(ch)
    
        for v := range ch {
            fmt.Println(v) // called twice
        }
    }
  • nil 的 channel 总是阻塞
    package main
    
    func main() {
        var ch chan bool
        ch <- true // blocks forever
    }
  • buffered/unbuffered channel 介于两者之间,会因为 channel 是否可以读写阻塞

那么究竟如何判断 channel 能否读写呢?答案就是 select。

select{
case channel_send_or_receive:
    //Dosomething
case channel_send_or_receive:
    //Dosomething
default:
    //Dosomething
}

协程退出

说了这么多,协程怎么退出呢?相信通过以上部分很容易得到结论:

  • 超时返回
  • 根据 channel 可读状态返回
 // 方式一:遍历关闭的 channel
for x := range closedCh {
    fmt.Printf("Process %d\n", x)
}
// 方式二:Select 可读 channel
for {
    select {
        case <-stopCh:
            fmt.Println("Recv stop signal")
            return
         case <-t.C:
            fmt.Println("Working .")
    }
}

完美退出

协程能够退出就够了么?还不够,完美的退出应该包含以下三点:

  • 通知协程退出
  • 通知确认,协程退出
  • 获取协程最终返回的错误

举个例子:errgroup

func (g *Group) Wait() error {
    g.wg.Wait()
    if g.cancel != nil {
        g.cancel()
    }
    return g.err
}

本文作者:cyningsun
本文地址https://www.cyningsun.com/01-31-2021/go-concurrency-goroutine-exit.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!

# Golang