防止Goroutine泄漏

最后更新于:2022-04-02 06:51:02

正如我们在“Goroutines”一节中介绍的那样,goroutines占用资源较少且易于创建。运行时将多个goroutine复用到任意数量的操作系统线程,以便我们不必担心抽象级别。但是他们会花费成本资源,并且goroutine不会被运行时垃圾收集,所以无论内存占用多少,我们都不想让他们对我们的进程撒谎。 那么我们如何去确保他们被清理干净? 让我们从头开始,一步一步思考:为什么会有一个goroutine? 在第二章中,我们确定,goroutines代表可能并行或不可以并行运行的工作单元。 该goroutine有几条路径终止: * 当它完成任务。 * 当它遇到不可恢复的错误无法继续它的任务。 * 当它被告知停止当前任务。 前两条我们已经知晓,可以通过算法实现。但如何取消当前任务?由于网络效应,这最重要的一点是:如果你已经开始了一个goroutine,那么它很可能以某种有组织的方式与其他几个goroutines合作。我们甚至可以把这种相互连接表现为一张图表,这时该goroutine能否停下来还取决于处在交互的其他goroutines。我们将在下一章中继续关注大规模并发产生的相互依赖关系,但现在让我们考虑如何确保保证单个goroutine得到清理。 让我们从一个简单的goroutine泄漏开始: ``` doWork := func(strings <-chan string) <-chan interface{} { completed := make(chan interface{}) go func() { defer fmt.Println("doWork exited.") defer close(completed) for s := range strings { fmt.Println(s) } }() return completed } doWork(nil) // 这里还有其他任务执行 fmt.Println("Done.") ``` 我们看到doWork被传递了一个nil通道。所以strings通道永远无法读取到其承载的内容,而且包含doWork的goroutine将在这个过程的整个生命周期中保留在内存中(如果我们在doWork和主goutoutine中加入了goroutine,我们甚至会死锁)。 在这个例子中,整个进程的生命周期很短,但是在一个真正的程序中,goroutines可以很容易地在一个长期生命的程序开始时启动,导致内存利用率下降。 解决这种情况的方法是建立一个信号,按照惯例,这个信号通常是一个名为done的只读通道。父例程将该通道传递给子例程,然后在想要取消子例程时关闭该通道。 这是一个例子: ``` doWork := func(done <-chan interface{}, strings <-chan string) <-chan interface{} { //1 terminated := make(chan interface{}) go func() { defer fmt.Println("doWork exited.") defer close(terminated) for { select { case s := <-strings: // Do something interesting fmt.Println(s) case <-done: //2 return } } }() return terminated } done := make(chan interface{}) terminated := doWork(done, nil) go func() { //3 // Cancel the operation after 1 second. time.Sleep(1 * time.Second) fmt.Println("Canceling doWork goroutine...") close(done) }() <-terminated //4 fmt.Println("Done.") ``` 1. 这里我们传递done通道给doWork函数。作为惯例,这个通道被作为首个参数。 2. 这里我们看到使用了for-select的使用模式之一。我们的目的是检查done通道有没有发出信号。如果有的话,我们退出当前goroutine。 3. 在这里我们创建另一个goroutine,一秒后就会取消doWork中产生的goroutine。 4. 这是我们在main goroutine中调用doWork函数返回结果的地方。 这会输出: ``` Canceling doWork goroutine... doWork exited. Done. ``` 你可以看到尽管向doWork传递了nil给strings通道,我们的goroutine依然正常运行至结束。与之前的例子不同,本例中我们把两个goroutine连接在一起之前,我们建立了第三个goroutine以取消doWork中的goroutine,并成功消除了泄漏问题。 前面的例子很好地处理了在通道上接收goroutine的情况,但是如果我们正在处理相反的情况:在尝试向通道写入值时阻塞goroutine会怎样? ``` newRandStream := func() <-chan int { randStream := make(chan int) go func() { defer fmt.Println("newRandStream closure exited.") // 1 defer close(randStream) for { randStream <- rand.Int() } }() return randStream } randStream := newRandStream() fmt.Println("3 random ints:") for i := 1; i <= 3; i++ { fmt.Printf("%d: %d\n", i, <-randStream) } ``` 1. 当goroutine成功执行时我们打印一行消息。 这会输出: ``` 3 random ints: 1: 5577006791947779410 2: 8674665223082153551 3: 6129484611666145821 ``` 你可以看到注释1所在的打印语句并未执行。在循环的第三次迭代之后,我们的goroutine块试图将下一个随机整数发送到不再被读取的通道。我们无法告知它停下来,解决方案是为生产者提供一条通知它退出的通道: ``` newRandStream := func(done <-chan interface{}) <-chan int { randStream := make(chan int) go func() { defer fmt.Println("newRandStream closure exited.") defer close(randStream) for { select { case randStream <- rand.Int(): case <-done: return } } }() return randStream } done := make(chan interface{}) randStream := newRandStream(done) fmt.Println("3 random ints:") for i := 1; i <= 3; i++ { fmt.Printf("%d: %d\n", i, <-randStream) } close(done) //模拟正在进行的工作 time.Sleep(1 * time.Second) ``` 这会输出: ``` 3 random ints: 1: 5577006791947779410 2: 8674665223082153551 3: 6129484611666145821 newRandStream closure exited. ``` 我们现在看到该goroutine被妥善清理。 现在我们知道如何确保goroutine不泄漏,我们可以制定一个约定:如果goroutine负责创建goroutine,它也负责确保它可以停止goroutine。 这个约定有助于确保程序在组合和扩展时可用。我们将在“管道”和“context包”中重新讨论这种技术和规则。我们该如何确保goroutine能够被停止根据goroutine的类型和用途而有所不同,但是它们 所有这些都是建立在传递done通道基础上的。 * * * * * 学识浅薄,错误在所难免。我是长风,欢迎来Golang中国的群(211938256)就本书提出修改意见。
';