为什么 goroutine 会泄漏?

huangapple go评论78阅读模式
英文:

Why goroutine leaks

问题

我阅读了《Twelve Go Best Practices》并在第30页遇到了一个有趣的例子。

func sendMsg(msg, addr string) error {
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        return err
    }
    defer conn.Close()
    _, err = fmt.Fprint(conn, msg)
    return err
}

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error)
    for _, addr := range addrs {
        go func(addr string) {
            errc <- sendMsg(msg, addr)
            fmt.Println("done")
        }(addr)
    }

    for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }
    return nil
}

func main() {
    addr := []string{"localhost:8080", "http://google.com"}
    err := broadcastMsg("hi", addr)

    time.Sleep(time.Second)

    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("everything went fine")
}

程序员提到了上述代码的情况:

  • goroutine 在 chan 写入时被阻塞
  • goroutine 持有 chan 的引用
  • chan 永远不会被垃圾回收

为什么 goroutine 在这里被阻塞?主线程被阻塞,直到它从 goroutine 接收到数据后才继续执行 for 循环,不是吗?

为什么 errc chan 永远不会被垃圾回收?因为我没有在 goroutine 完成后关闭通道吗?

英文:

I read Twelve Go Best Practices and encounter and interesting example on page 30.

func sendMsg(msg, addr string) error {
    conn, err := net.Dial(&quot;tcp&quot;, addr)
    if err != nil {
        return err
    }
    defer conn.Close()
    _, err = fmt.Fprint(conn, msg)
    return err
} 

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error)
    for _, addr := range addrs {
        go func(addr string) {
            errc &lt;- sendMsg(msg, addr)
            fmt.Println(&quot;done&quot;)
        }(addr)
    }

    for _ = range addrs {
        if err := &lt;-errc; err != nil {
            return err
        }
    }
    return nil
}

func main() {
    addr := []string{&quot;localhost:8080&quot;, &quot;http://google.com&quot;}
    err := broadcastMsg(&quot;hi&quot;, addr)

    time.Sleep(time.Second)

    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(&quot;everything went fine&quot;)
}

The programmer mentioned, that happens to the code above:

the goroutine is blocked on the chan write
the goroutine holds a reference to the chan
the chan will never be garbage collected

Why the goroutine is blocked here? The main thread is blocked, until it receive data from goroutine. After it continues the for loop. Not?

Why the errc chan will be never garbage collected? Because I do not close the channel, after goroutine is finished?

答案1

得分: 10

我看到的一个问题是,在broadcastMsg()函数内部,在goroutine启动后:

for _ = range addrs {
    if err := <-errc; err != nil {
        return err
    }
}

如果从errc通道接收到非nilerrorbroadcastMsg()函数会立即返回该错误,并且不会从通道接收更多的值,这意味着后续的goroutine将永远不会被解除阻塞,因为errc是无缓冲的。

可能的修复方法

一个可能的修复方法是使用一个足够大的缓冲通道,以确保不会阻塞任何一个goroutine,例如:

errc := make(chan error, len(addrs))

或者,即使从通道接收到非nilerror,仍然继续接收与goroutine发送次数相同的次数:

var errRec error
for _ = range addrs {
    if err := <-errc; err != nil {
        if errRec == nil {
            errRec = err
        }
    }
}
return errRec

或者如链接中的演讲中所提到的第33张幻灯片:使用一个“quit”通道,在broadcastMsg()完成/返回后防止已启动的goroutine保持阻塞状态。

英文:

One problem I see is that inside broadcastMsg() after goroutines have started:

for _ = range addrs {
    if err := &lt;-errc; err != nil {
        return err
    }
}

If a non-nil error is received from errc, broadcastMsg() returns immediately with that error and does not receive futher values from the channel, which means further goroutines will never get unblocked because errc is unbuffered.

Possible Fixes

A possible fix would be to use a buffered channel, big enough to not block any of the goroutines, in this case:

errc := make(chan error, len(addrs))

Or even if a non-nil error is received from the channel, still proceed to receive as many times as many goroutines send on it:

var errRec error
for _ = range addrs {
    if err := &lt;-errc; err != nil {
        if errRec == nil {
            errRec = err
        }
    }
}
return errRec

Or as mentioned in the linked talk on slide #33: use a "quit" channel to prevent the started goroutines to remain blocked after broadcastMsg() has completed/returned.

答案2

得分: 5

你有一个包含两个地址(localhost、google)的列表。对于每个地址,你都使用一个goroutine发送一条消息(hi)。这个goroutine会将错误(可能为nil)发送到errc通道。

如果你向一个通道发送了内容,你也需要有一个读取该通道值的机制,否则它会阻塞(除非它是一个带缓冲区的通道,但即使是带缓冲区的通道,一旦缓冲区满了也会阻塞)。

所以你的读取循环看起来像这样:

for _ = range addrs {
    if err := <-errc; err != nil {
        return err
    }
}

如果第一个地址返回的错误不为nil,循环就会返回。后续的错误值从通道中没有被读取,因此它会阻塞。

英文:

You have a list of two addresses (localhost, google). To each of these you're sending a message (hi), using one goroutine per address. And the goroutine sends error (which may be nil) to errc channel.

If you send something to a channel, you also need something that reads the values from that channel, otherwise it will block (unless it's a buffered channel, but even buffered channels block once their buffer is full).

So your reading loop looks like this:

for _ = range addrs {
    if err := &lt;-errc; err != nil {
        return err
    }
}

If the first address returns an error which is not nil, the loop returns. The subsequent error values are never read from the channel thus it blocks.

huangapple
  • 本文由 发表于 2015年4月27日 18:16:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/29892950.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定