防止Goroutine泄漏:GO开发人员的最佳实践和技巧
#go #并发性 #designpatterns

这是Golang的一系列并发设计模式中的第一个博客。我将添加更多具有许多复杂设计模式的博客。如果您想通知,请订阅我的Newsletter

从设计模式开始之前,这里有一些先决条件。

  • 如果您对Golang的语法感到满意,那将是最好的。

让我们在讨论设计模式之前先仔细考虑一些概念。 (如果您知道所有这些都可以随意跳到设计模式)

goroutines

简单地说,goroutine是与其他代码同时运行的代码的一部分。我们不会涉及太多理论(我们可以将其留给其他博客)。

要声明goroutine,您只需要在调用功能时使用GO前缀。

这是一个简单的示例:

func main(){

go func(){
    for i:= 0 ;i<5;i++{
        fmt.Print(i + " ")
    }
}()

go func(){
    for i:= 5 ;i<10;i++{
        fmt.Println(i + " ")
    }
}()
}

此代码段的输出将是这样的:

0 1 5 2 6 7 3 8 9 4
# It does not have to be exactly same.

频道

通道是我们并发设计模式中最重要的构建块之一。简单地说

通道只是可用于将数据从一个goroutine发送到另一个的排队。

以下是有关渠道的一些重要事实:

  • 频道可以是缓冲(可以容纳一个或多个值)或 (不能容纳任何值)
  • 频道正在阻止。
    • 如果您将对象推向填充的通道,则该程序将在从频道弹出另一个对象
    • 之前继续进行。
    • 如果您是从空通道中弹出元素,则该程序将在将对象推向频道之前不会进一步进行。
// This is a read channel. You can only read values
var readChannel <-chan interface{} 

// This is a write channel. You can only write values to this channel
var writeChannel chan<- interface{}

//You can read as well as write to this channel
var channel chan interface{}

channel1 <- val // we are pushing a value to a channel
val <- channel2 // we are reading from a channel

现在让我们跳到有趣的部分

什么是Goroutine泄漏?

我们使用goroutine执行一些操作,一段时间后,将结果发送给我们并终止。但是,如果不终止会发生什么?好吧,这就是我们所说的goroutine泄漏。

即使goroutines轻巧,如果我们调用很多goroutines,我们也会浪费很多资源。

在讨论之前,我们将如何防止Goroutine泄漏,让我们检查一个例子:

func test() <-chan int {
    outStream := make(chan int)
    go func(outStream chan<- int) {
        for i := 0; i < 10; i-- {
            outStream <- i
        }
    }(outStream)
    return outStream
}

func main() {
    inStream := test()
    for i := 0; i < 10; i++ {
        val := <-inStream
        fmt.Println(val)
        time.Sleep(1 * time.Second)
    }
}

在这里,我们有一个 test 函数返回读取通道(我们只能从此通道读取值)。然后,它调用goroutine并以写入通道的方式传递通道(我们只能写入此通道)。我们正在运行一个用于循环的循环,并将值写入频道。但这是有趣的部分, for循环是无限的!我们已经这样做是为了模仿我们的goroutine不会终止并保持价值的情况。

运行代码时,我们将获得以下输出:

Image description

我们在10次迭代后停止了循环,但可以永远持续。

我们在10次迭代后停止了循环,但它可以永远持续。

  • 要阻止Goroutine永远执行,父母需要发送一些信号。 是父母,我们的意思是调用goroutine的函数。

  • 当孩子收到信号时,它会停止操作。由于Goroutines只能通过频道进行通信,因此父母会将频道发送到其子女Goroutine。

  • 父母要终止孩子后,它就可以关闭频道。孩子收到此信号,还终止了其动作。

让我们看一下代码实现以更好地理解它

func test(terminate <-chan bool) <-chan int {
    outStream := make(chan int)
    go func(terminate <-chan bool, outStream chan<- int) {
        defer close(outStream) // Channel is closed when the function is finishes executing
        for i := 0; i < 10; i-- {
            select {
            case <-terminate:
                fmt.Println("Child Terminating")
                return
            case outStream <- i:
            }
        }
    }(terminate, outStream)
    return outStream
}

func main() {
    terminate := make(chan bool)

    inStream := test(terminate)
    for i := 0; i < 10; i++ {
        val, ok := <-inStream

       // If !ok, then it means channel is closed and we won't receive new value
        if !ok {
            fmt.Println("Channel closed")
            break
        }

        fmt.Println(val)
        time.Sleep(1 * time.Second)
        if i == 5 {
            close(terminate)
        }
    }
}

好吧,让我们讨论此代码的工作原理

  • 我们制作了一个名为'terminate'的新频道,并将其作为只读渠道传递给孩子。

  • 在无限的循环中,我们使用选择语句。 (Select语句就像开关语句一样,它在相应的

  • 时执行代码块
  • 现在,当频道被父母关闭时,将闭合信号发送给孩子。然后,它执行其相应的代码块,即返回语句。打破了无限环路。

  • 现在,Goroutine终止了,关闭了“ Outstream”频道,从而终止了孩子。

现在,如果我们执行代码,它看起来像这样:

Image description

感谢您的阅读直到最后。如果您喜欢我的博客,请订阅我的newsletter以获取更多很棒的内容。

封面图片来源:https://www.storj.io/blog/finding-goroutine-leaks-in-tests