分享

常见的 Goroutine 泄露,你应该避免

 技术的游戏 2023-09-14 发布于广东

永远不要启动一个 Goroutine,除非你知道它如何停止。

img

照片来自 Luis Quintero 在 Pexels

Go 语言编写代码的最大优点之一是能够在轻量级线程,即 Goroutines 中并发运行你的代码。

然而,拥有强大的能力也伴随着巨大的责任。

尽管 Goroutines 非常方便,但如果不小心处理,它们很容易引入难以追踪的错误。

Goroutine 泄露就是其中之一。它在背景中悄悄增长,可能最终在你不知情的情况下使你的应用程序崩溃。

因此,本文主要介绍 Goroutine 泄露是什么,以及你如何防止泄露发生。

我们来看看吧!

什么是 Goroutine 泄露?

img

照片来自 Wallpaper Access

当创建一个新的 Goroutine 时,计算机在堆中分配内存,并在执行完成后释放它们。

Goroutine 泄露是一种内存泄露,当 Goroutine 没有终止并在应用程序的生命周期中被留在后台时就会发生。

让我们来看一个简单的例子。

func goroutineLeak(ch chan int) {
    data := <- ch
    fmt.Println(data)
}

func handler() {
    ch := make(chan int)
    
    go goroutineLeak(ch)
    return
}

随着处理器的返回,Goroutine 继续在后台活动,阻塞并等待数据通过通道发送 —— 这永远不会发生。

因此,产生了一个 Goroutine 泄露。

在本文中,我将引导你了解两种常见的模式,这些模式很容易导致 Goroutine 泄漏:

  • · 遗忘的发送者

  • · 被遗弃的接收者

让我们深入研究!

1.0 遗忘的发送者

img

照片来自 Engin Akyurt 在 Pexels

遗忘的发送者发生在发送者被阻塞,因为没有接收者在通道的另一侧等待接收数据的情况。

func forgottenSender(ch chan int) {
    data := 3
  
    // This is blocked as no one is receiving the data
    ch <- data
}

func handler () {
    ch := make(chan int)
  
    go forgottenSender(ch)
    return
}

虽然它起初看起来很简单,但在以下两种情境中很容易被忽视。

不当使用 Context

func forgottenSender(ch chan int) {
    data := networkCall()
  
    ch <- data
}

func handler() error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
    defer cancel()
  
    ch := make(chan int)
    go forgottenSender(ch)
  
    select {
        case data := <- ch: {
            fmt.Printf("Received data! %s", data)
      
            return nil
        }
    
        case <- ctx.Done(): {
            return errors.New("Timeout! Process cancelled. Returning")
        }
    }
}

在上面的例子中,我们模拟了一个标准的网络服务处理程序。

我们定义了一个上下文,它在10ms后发出超时,随后是一个异步进行网络调用的Goroutine。

select语句等待多个通道操作。它会阻塞,直到其其中一个情况可以运行并执行该情况。

如果网络调用完成之前超时到达,case <- ctx.Done() 将会执行,处理程序将返回一个错误。

当处理程序返回时,不再有任何接收者等待接收数据。forgottenSender将被阻塞,等待有人接收数据,但这永远不会发生!

这就是Goroutine泄露的地方。

错误检查后的接收者位置

这是另一个典型的情况。

func forgottenSender(ch chan int) {
    data := networkCall()
  
    ch <- data
}

func handler() error {
    ch := make(chan int)
    go forgottenSender(ch)
  
    err := continueToValidateOtherData()
    if err != nil {
        return errors.New("Data is invalid! Returning.")
    }
  
    data := <- ch
  
    return nil
}

在上面的例子中,我们定义了一个处理程序并生成一个新的Goroutine来异步进行网络调用。

在等待调用返回的过程中,我们继续其他的验证逻辑。

如你所见,当continueToValidateOtherData返回一个错误导致处理程序返回时,泄露就发生了。

没有人等待接收数据,forgottenSender将永远被阻塞!

解决方案: 忘记的发送者

使用一个缓冲通道。

如果你回想一下,忘记的发送者发生是因为另一端没有接收者。阻塞问题的罪魁祸首是一个无缓冲的通道!

一个无缓冲的通道是在消息发出时立即需要一个接收者的,否则发送者会被阻塞。它是在没有为通道分配容量的情况下声明的。

func forgottenSender(ch chan int) {
    data := 3
  
    // This will NOT block
    ch <- data
}

func handler() {
    // Declare a BUFFERED channel
    ch := make(chan int1)
  
    go forgottenSender(ch)
    return
}

通过为通道添加特定的容量,在这种情况下为1,我们可以减少所有提到的问题。

发送者可以在不需要接收者的情况下将数据注入通道。

2.0 被遗弃的接收者

img

照片由Jeswin ThomasPexels上发布。

正如其名字所暗示的,被遗弃的接收者是完全相反的情况。

当一个接收者被阻塞,因为另一边没有发送者发送数据时,它就会发生。

func abandonedReceiver(ch chan int) {
    // This will be blocked
    data := <- ch
  
    fmt.Println(data) 
}

func handler() {
    ch := make(chan int)
  
    go abandonedReceiver(ch)
  
    return
}

第3行一直被阻塞,因为没有发送者发送数据。

让我们再次了解两个常见的场景,这些场景经常被忽视。

发送者未关闭的通道

func abandonedWorker(ch chan string) {
    for data := range ch {
        processData(data)
    }
  
    fmt.Println("Worker is done, shutting down")
}

func handler(inputData []string) {
    ch := make(chan stringlen(inputData))
  
    for _, data := range inputData {
        ch <- data
    }
  
    go abandonedWorker(ch)
    
    return
}

在上面的例子中,处理程序接收一个字符串切片,创建一个通道并将数据插入到通道中。

处理程序然后通过Goroutine启动一个工作程序。工作程序预计会处理数据,并且一旦处理完通道中的所有数据,就会终止。

然而,即使消耗并处理了所有的数据,工作程序也永远不会到达“第6行”!

尽管通道是空的,但它没有被关闭!工作程序继续认为未来可能会有传入的数据。因此,它坐下来并永远等待。

这是Goroutine再次泄漏的地方。

在错误检查之后放置发送者

这与我们之前的一些示例非常相似。

func abandonedWorker(ch chan []int) {
    data := <- ch

    fmt.Println(data)
}

func handler() error {
    ch := make(chan []int)
    go abandonedWorker(ch)

    records, err := getFromDB()
    if err != nil {
        return errors.New("Database error. Returning")
    }

    ch <- records

    return nil
}

在上面的例子中,处理程序首先启动一个Goroutine工作程序来处理和消费一些数据。

然后,处理程序从数据库中查询记录,然后将记录注入通道供工作程序使用。

如果数据库出现错误,处理程序将立即返回。通道将不再有任何发送者传入数据。

因此,工作程序被遗弃。

解决方案:被遗弃的接收者

在这两种情况下,接收者都被留下,因为他们“认为”通道将有传入的数据。因此,它们阻塞并永远等待。

解决方案是一个简单的单行代码。

defer close(ch)

当你启动一个新的通道时,最好的做法是推迟关闭通道。

这确保在数据发送完成或函数退出时关闭通道。

接收者可以判断一个通道是否已关闭,并相应地终止。

func abandonedReceiver(ch chan int) {
    // This will NOT be blocked FOREVER
    data := <- ch
  
    fmt.Println(data) 
}

func handler() {
    ch := make(chan int)
  
      // Defer the CLOSING of channel
    defer close(ch)
  
    go abandonedReceiver(ch)
    return
}

结论

img

照片由 Pixabay 提供,在 Pexels 上发布。

关于 Goroutine 泄漏就是这么多了!

尽管它不像其他 Goroutine 错误那么强大,但这种泄漏仍然会大大耗尽应用程序的内存使用。

记住,拥有强大的力量也伴随着巨大的责任。

保护我们的应用程序免受错误的责任在于你我——开发人员!

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多