go Goroutine泄露

 

泄露情况分类

 

死循环

go协程单纯地陷入死循环中。

 

chanel 引起的泄露

发送不接收

向没有接收者的channel发送信息。
我们知道,发送者一般都会配有相应的接收者。理想情况下,我们希望接收者总能接收完所有发送的数据,这样就不会有任何问题。但现实是,一旦接收者发生异常退出,停止继续接收上游数据,发送者就会被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
 
import
    "fmt" 
    "math/rand" 
    "runtime" 
    "time" 
)
 
func query() int { 
    n := rand.Intn(100) 
    time.Sleep(time.Duration(n) * time.Millisecond) 
    return
}
 
func queryAll() int { 
    ch := make(chan int) 
    go func() { ch <- query() }() 
    go func() { ch <- query() }() 
    go func() { ch <- query() }() 
    return <-ch 
}
 
func main() { 
    for i := 0; i < 4; i++ { 
        queryAll() 
        fmt.Printf("#goroutines: %d", runtime.NumGoroutine()) 
    
}

输出:

 

1
2
3
4
#goroutines: 3
#goroutines: 5
#goroutines: 7
#goroutines: 9

每次调用 queryAll 后,goroutine 的数目会发生增长。问题在于,在接收到第一个响应后,“较慢的” goroutine 将会发送到另一端没有接收者的 channel 中。

可能的解决方法是,如果提前知道后端服务器的数量,那么使用缓存 channel。否则,只要至少有一个 goroutine 仍在工作,我们就可以使用另一个 goroutine 来接收来自这个 channel 的数据。其他的解决方案可能是使用 context(example),利用 某些机制来取消其他请求。

 

接收不发送

从没有发送者的channel中接收信息。

发送不接收会导致发送者阻塞,反之,接收不发送也会导致接收者阻塞。直接看示例代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
 
func main() {
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()
 
    var ch chan struct{}
    go func() {
        ch <- struct{}{}
    }()
}

运行结果显示:

1
the number of goroutines: 2

当然,我们正常不会遇到这么傻的情况发生,现实工作中的案例更多可能是发送已完成,但是发送者并没有关闭 channel,接收者自然也无法知道发送完毕,阻塞因此就发生了。

解决方案是什么?那当然就是,发送完成后一定要记得关闭 channel。

 

nil channel
向 nil channel 发送和接收数据都将会导致阻塞。这种情况可能在我们定义 channel 时忘记初始化的时候发生。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()
 
    var ch chan int
    go func() {
        <-ch
        // ch<-
    }()
}

两种写法:<-ch 和 ch<- 1,分别表示接收与发送,都将会导致阻塞。如果想实现阻塞,通过 nil channel 和 done channel 结合实现阻止 main 函数的退出,这或许是可以一试的方法。

 

传统同步机制

传统同步机制主要指面向共享内存的同步机制,比如排它锁、共享锁等。这两种情况导致的泄露还是比较常见的。go 由于 defer 的存在,第二类情况,一般情况下还是比较容易避免的。
虽然,一般推荐 Go 并发数据的传递,但有些场景下,显然还是使用传统同步机制更合适。Go 中提供传统同步机制主要在 sync 和 atomic 两个包。接下来,我主要介绍的是锁和 WaitGroup 可能导致 goroutine 的泄露。

Mutex
和其他语言类似,Go 中存在两种锁,排它锁和共享锁,关于它们的使用就不作介绍了。我们以排它锁为例进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
    total := 0
 
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("total: ", total)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()
 
    var mutex sync.Mutex
    for i := 0; i < 2; i++ {
        go func() {
            mutex.Lock()
            total += 1
        }()
    }
}

执行结果如下:

1
2
total: 1
the number of goroutines: 2

这段代码通过启动两个 goroutine 对 total 进行加法操作,为防止出现数据竞争,对计算部分做了加锁保护,但并没有及时的解锁,导致 i = 1 的 goroutine 一直阻塞等待 i = 0 的 goroutine 释放锁。可以看到,退出时有 2 个 goroutine 存在,出现了泄露,total 的值为 1。

怎么解决?因为 Go 有 defer 的存在,这个问题还是非常容易解决的,只要记得在 Lock 的时候,记住 defer Unlock 即可。

示例如下:

mutex.Lock()
defer mutext.Unlock()

其他的锁与这里其实都是类似的。

WaitGroup

WaitGroup 和锁有所差别,它类似 Linux 中的信号量,可以实现一组 goroutine 操作的等待。使用的时候,如果设置了错误的任务数,也可能会导致阻塞,导致泄露发生。

一个例子,我们在开发一个后端接口时需要访问多个数据表,由于数据间没有依赖关系,我们可以并发访问,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main
 
import (
    "fmt"
    "runtime"
    "sync"
    "time"
)
 
func handle() {
    var wg sync.WaitGroup
 
    wg.Add(4)
 
    go func() {
        fmt.Println("访问表1")
        wg.Done()
    }()
 
    go func() {
        fmt.Println("访问表2")
        wg.Done()
    }()
 
    go func() {
        fmt.Println("访问表3")
        wg.Done()
    }()
 
    wg.Wait()
}
 
func main() {
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()
 
    go handle()
    time.Sleep(time.Second)
}

执行结果如下:

the number of goroutines: 2

出现了泄露。再看代码,它的开始部分定义了类型为 sync.WaitGroup 的变量 wg,设置并发任务数为 4,但是从例子中可以看出只有 3 个并发任务。故最后的 wg.Wait() 等待退出条件将永远无法满足,handle 将会一直阻塞。

怎么防止这类情况发生?

我个人的建议是,尽量不要一次设置全部任务数,即使数量非常明确的情况。因为在开始多个并发任务之间或许也可能出现被阻断的情况发生。最好是尽量在任务启动时通过 wg.Add(1) 的方式增加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
wg.Add(1)
go func() {
    fmt.Println("访问表1")
    wg.Done()
}()
 
wg.Add(1)
go func() {
    fmt.Println("访问表2")
    wg.Done()
}()
 
wg.Add(1)
go func() {
    fmt.Println("访问表3")
    wg.Done()
}()
...

  

refer:

Goroutine 泄露

Go 笔记之如何防止 goroutine 泄露

 

posted @   -零  阅读(775)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
历史上的今天:
2019-02-22 Django之Models(一)
点击右上角即可分享
微信分享提示