8

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

 3 years ago
source link: https://www.cyningsun.com/01-31-2021/go-concurrency-goroutine-exit.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

在 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 许可协议。转载请注明出处!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK