• 造成Goroutine泄露的原因有哪些?
  • 发布于 2个月前
  • 148 热度
    0 评论
Go 语言编写代码的最大优点之一是能够在轻量级线程,即 Goroutines 中并发运行你的代码。然而,拥有强大的能力也伴随着巨大的责任。尽管 Goroutines 非常方便,但如果不小心处理,它们很容易引入难以追踪的错误。Goroutine 泄露就是其中之一。它在背景中悄悄增长,可能最终在你不知情的情况下使你的应用程序崩溃。因此,本文主要介绍 Goroutine 泄露是什么,以及你如何防止泄露发生。

我们来看看吧!

什么是 Goroutine 泄露?
当创建一个新的 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 遗忘的发送者
遗忘的发送者发生在发送者被阻塞,因为没有接收者在通道的另一侧等待接收数据的情况。
func forgottenSender(ch chan int) {
    data := 3
    // 堆代码 duidaima.com
    // 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 int, 1)
  
    go forgottenSender(ch)
    return
}
通过为通道添加特定的容量,在这种情况下为1,我们可以减少所有提到的问题。发送者可以在不需要接收者的情况下将数据注入通道。

2.0 被遗弃的接收者
正如其名字所暗示的,被遗弃的接收者是完全相反的情况。当一个接收者被阻塞,因为另一边没有发送者发送数据时,它就会发生。
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 string, len(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
}

结论
关于 Goroutine 泄漏就是这么多了!尽管它不像其他 Goroutine 错误那么强大,但这种泄漏仍然会大大耗尽应用程序的内存使用。记住,拥有强大的力量也伴随着巨大的责任。保护我们的应用程序免受错误的责任在于你我——开发人员!
用户评论