【问题标题】:Multiple go routines consuming from a channel causing loss of data从一个通道消耗多个 goroutine 导致数据丢失
【发布时间】:2018-02-05 11:35:56
【问题描述】:

我是 Go 新手。在下面的示例中,多个 go 例程正在从无缓冲的通道中消耗。

代码:

var c = make(chan int)

func f() {
    for val := range c {    
        fmt.Printf("routine 1 : %v\n", val)
    }
}   

func g() {
    fmt.Printf("routine 2 : %v\n", <-c)
}

func main() {
    go f()
    go g()
    c <- 0
    c <- 1
    c <- 2
    c <- 3
    c <- 4
    c <- 5
    close(c) 
}

输出是:

routine 1 : 0
routine 1 : 2
routine 2 : 1
routine 1 : 3
routine 1 : 4

这里缺少值 5 并且永远不会被打印出来!为什么会这样?如果我删除呼叫 - go g(),它会完美运行。

另外,如果我让通道缓冲,说:

var c = make(chan int, 10)

根本没有输出。我知道对于无缓冲的通道,发送在接收完成后完成,而缓冲的情况并非如此。不过,对于缓冲的情况,如果 channel 还没有发送任何 int,那么考虑到它是 nil 通道,for 循环不会被阻塞吗?

请帮助解决我的两个问题。感谢所有的投入。

【问题讨论】:

标签: go goroutine


【解决方案1】:

你需要等待 goroutine 完成


方法一:使用sync.WaitGroup

// A WaitGroup waits for a collection of goroutines to finish.
// The main goroutine calls Add to set the number of
// goroutines to wait for. Then each of the goroutines
// runs and calls Done when finished. At the same time,
// Wait can be used to block until all goroutines have finished.

试试this:

package main

import (
    "fmt"
    "sync"
)

var c = make(chan int)
var wg sync.WaitGroup

func f() {
    defer wg.Done()
    for val := range c {
        fmt.Printf("routine 1 : %v\n", val)
    }
}

func g() {
    defer wg.Done()
    if data, ok := <-c; ok {
        fmt.Println("routine 2 :", data)
    }
}

func main() {
    wg.Add(2)
    defer wg.Wait()
    go f()
    go g()
    c <- 0
    c <- 1
    c <- 2
    c <- 3
    c <- 4
    c <- 5
    close(c)
}

样本输出:

routine 2 : 0
routine 1 : 1
routine 1 : 2
routine 1 : 3
routine 1 : 4
routine 1 : 5

方法二:使用退出频道,试试this

package main

import "fmt"

func main() {
    go f()
    go g()
    c <- 0
    c <- 1
    c <- 2
    c <- 3
    c <- 4
    c <- 5
    close(c)
    <-quit
    <-quit
}
func f() {
    defer done()
    for val := range c {
        fmt.Printf("routine 1 : %v\n", val)
    }
}
func g() {
    defer done()
    if data, ok := <-c; ok {
        fmt.Println("routine 2 :", data)
    }
}
func done() {
    quit <- struct{}{}
}

var c = make(chan int)
var quit = make(chan struct{}, 2)

输出:

routine 2 : 0
routine 1 : 1
routine 1 : 2
routine 1 : 3
routine 1 : 4
routine 1 : 5

【讨论】:

  • @Mayank:我希望这会有所帮助。
  • 这段代码看起来不正确,它仍然可以退出而不打印,因为wg.Wait 将等待最多两个 go 例程执行(可能超过 2 个值,bcos 在它可以的例程之一中读取多个值),完成(而不是 5 个)。
  • @Ravi 使用sync.WaitGroup 是惯用的方式。这里只有两个额外的 goroutine 需要等待而不是 5 个!
  • 抱歉,评论错误,但您的代码不正确。让我们理解一下:假设 goroutine g 只有在 goroutine f 消耗完所有发送的值之后才会执行。最后 wg.Wait 仍然会等待,直到 g 中的 wg.Done 被执行。现在,在 g 中读取的通道将收到一个 0 值(因为通道已关闭,但该值从未发送过!)并错误地将其打印出来。这是您的代码,在 (g) 中添加了睡眠以进行模拟:play.golang.org/p/L98ZNhV-XA。为了比较在我之前共享的代码中添加了一个睡眠(到 g) - 与您的输入相同:play.golang.org/p/MXV7_pDAye
  • @Ravi / @ A.R - 感谢您的详细解释并概述了正确的解决方案。帮助很大:)
【解决方案2】:

@Flimzy 提到的内容是正确的,在读取结束后发送解除阻塞并且主 go 例程在关闭通道后退出,甚至在打印完成之前(有时它可能会完成,所以你可能会偶尔看到它)。一旦主 go 例程退出,所有其他 go 例程也会退出。这是一个使用WaitGroup 进行同步https://play.golang.org/p/i2uHw3X1G3 的解决方案 - 这样您就不会看到任何遗漏。希望这会有所帮助。

对于缓冲通道,主 go 例程在写入值后不必等待读取完成,因此它在发送所有值后退出。同样,您需要为此使用同步。

【讨论】:

    【解决方案3】:

    只要5 被消耗,程序就会退出。它没有时间打印输出。如果您运行该程序足够多次,您可能会发现在某些情况下,它确实会在关闭之前打印输出,但它完全是随机的。

    在退出程序之前,您需要添加一些机制来等待您的频道完成。

    【讨论】:

      猜你喜欢
      • 2020-08-02
      • 2019-02-20
      • 2019-10-08
      • 1970-01-01
      • 1970-01-01
      • 2021-08-15
      • 2015-12-17
      • 1970-01-01
      • 2015-06-03
      相关资源
      最近更新 更多