如何在 Golang 的 goroutines 中安全地与通道交互

How to safely interact with channels in goroutines in Golang

我是新手,我想了解 goroutines 中通道的工作方式。据我了解,关键字 range 可用于迭代通道的值,直到通道关闭或缓冲区 运行 结束;因此,for range c 将重复循环,直到缓冲区 运行 结束。

我有以下简单的功能可以增加频道的价值:

func main() {

    c := make(chan int)
    go printchannel(c)
    for i:=0; i<10 ; i++ {
        c <- i
    }

}

我有两个 printchannel 的实现,我不确定行为为何不同。

实施 1:

func printchannel(c chan int) {
    for range c {
        fmt.Println(<-c)
    }
}

输出:1 3 5 7

实施 2:

func printchannel(c chan int) {
    for i:=range c {
        fmt.Println(i)
    }
}

输出:0 1 2 3 4 5 6 7 8

我没想到这些输出!

想要的输出:0 1 2 3 4 5 6 7 8 9

不应该 main 函数和 printchannel 函数 运行 在两个并行线程上,一个向通道添加值,另一个读取值直到通道关闭?我可能在这里遗漏了一些基本的 go/thread 概念,指向这些概念的指针会有所帮助。

非常感谢对此的反馈(以及我对 goroutine 中通道操作的理解)!

您的第一个实施仅 returns 每隔一个数字的原因是因为每次循环运行时您实际上 "taking" 来自 c 两次:首先是 range,然后再次使用 <-。碰巧你实际上并没有绑定或使用从通道中取出的第一个值,所以你最终打印的是所有其他值。

第一个实现的另一种方法是根本不使用 range,例如:

func printchannel(c chan int) {
    for {
        fmt.Println(<-c)
    }
}

我无法在我的机器上复制你的第二个实现的行为,但原因是你的两个实现都是活泼的——它们将在 main 结束时终止,无论什么数据可能在等待中channel 或许多 goroutines 可能处于活动状态。

作为结束语,我警告您不要将 goroutine 明确视为 "threads",尽管它们具有相似的思维模型和界面。在像这样的简单程序中,Go 完全可能只使用单个 OS 线程来完成所有工作。

您的第一个循环不起作用,因为您有 2 个阻塞通道接收器并且它们不会同时执行。

当您调用 goroutine 时,循环开始,它等待第一个值被发送到通道。有效地将其视为 <-c

当 main 函数中的 for 循环运行时,它在 Chan 上发送 0。此时 range c 收到值并停止阻止循环的执行。

然后它被 fmt.println(<-c) 的接收方阻止。当 1 在 main 中的循环的第二次迭代中发送时,在 fmt.println(<-c) 处接收到的数据会从通道读取,从而允许 fmt.println 执行,从而完成循环并等待 for range c 处的值.

你对循环机制的第二种实现是正确的。 它在打印到 9 之前退出的原因是在 main 中的 for 循环完成后程序继续并完成 main.

的执行

在 Go 中,func main 在执行时作为 goroutine 本身启动。因此,当 main 中的 for 循环完成时,它会继续并退出,并且由于打印在一个关闭的并行 goroutine 中,因此它永远不会执行。它没有时间打印,因为没有什么可以阻止 main 完成和退出程序。

解决这个问题的一种方法是使用等待组http://www.golangprograms.com/go-language/concurrency.html

为了获得预期的结果,您需要在 main 中有一个阻塞进程 运行 以提供足够的时间或等待 goroutine 执行的确认,然后才允许程序继续。

实施 1. 您从频道读取了两次 - range c<-c 都从频道读取。

实施2。这是正确的做法。您可能看不到 9 打印的原因是两个 goroutines 可能 运行 在并行线程中。在那种情况下,它可能是这样的:

  1. main goroutine 将 9 发送到通道并阻塞直到它被读取
  2. 第二个 goroutine 从通道接收 9
  3. main goroutine 解除阻塞并退出。这终止了整个程序,它不给第二个 goroutine 机会打印 9

在这种情况下,您必须同步您的 goroutine。比如像这样

func printchannel(c chan int, wg *sync.WaitGroup) {
    for i:=range c {
        fmt.Println(i)
    }

    wg.Done() //notify that we're done here
}

func main() {
    c := make(chan int)
    wg := sync.WaitGroup{}

    wg.Add(1) //increase by one to wait for one goroutine to finish
              //very important to do it here and not in the goroutine
              //otherwise you get race condition

    go printchannel(c, &wg) //very important to pass wg by reference
                            //sync.WaitGroup is a structure, passing it
                            //by value would produce incorrect results

    for i:=0; i<10 ; i++ {
        c <- i
    }

    close(c)  //close the channel to terminate the range loop
    wg.Wait() //wait for the goroutine to finish
}

关于 goroutines 与线程。你不应该混淆它们并且可能应该理解它们之间的区别。 Goroutines 是绿色线程。关于该主题的博客文章、讲座和 Whosebug 答案数不胜数。

在实现 1 中,range 读入通道一次,然后再次读入 Println。因此,您将跳过 2、4、6、8。

在这两种实现中,一旦最后的 i (9) 被发送到 goroutine,程序就会退出。因此 goroutine 没有时间打印出 9。要解决这个问题,请使用另一个答案中提到的 WaitGroup,或者使用 done 通道来避免 semaphore/mutex.

func main() {

    c := make(chan int)
    done := make(chan bool)
    go printchannel(c, done)
    for i:=0; i<10 ; i++ {
        c <- i
    }
    close(c)
    <- done
}

func printchannel(c chan int, done chan bool) {
    for i := range c {
        fmt.Println(i)
    }
    done <- true
}