Golang通道输出顺序

9
func main() {
  messages := make(chan string)
  go func() { messages <- "hello" }()
  go func() { messages <- "ping" }()
  msg := <-messages
  msg2 := <-messages
  fmt.Println(msg)
  fmt.Println(msg2)

上述代码在我的终端上一致打印出“ping”和“hello”。 我对打印顺序感到困惑,因此想知道我思考的问题是否正确。
我了解未缓冲通道在等待发送者和接收者时会阻塞。因此,在上述情况下,当这两个Go例程被执行时,都没有接收器。因此,我猜想两个例程都会阻塞,直到通道上有接收器可用。
现在...我假设首先尝试将“hello”放入通道中,但必须等待...同时,“ping”也尝试了,但同样需要等待。然后
msg := <- messages

出现时,我会假设在这个阶段,程序将随意选择一个等待的 goroutine 并允许它将其消息发送到通道中,因为 msg 已经准备好接收。

然而,无论我运行程序多少次,似乎总是 msg 被分配为 "ping",而 msg2 被分配为 "hello",这给人留下了 "ping" 总是优先发送(到 msg)的印象。为什么会这样呢?


你想要什么?你想同步这些频道,以便hello总是先打印出来吗? - Himanshu
2
你的观察是正确的,在线程执行顺序方面(在golang中,即go routines),并没有被设计成可预测的。如果你想以特定的方式控制执行流程,也许这篇文章(https://dev59.com/xlkS5IYBdhLWcg3wdmnC)可以帮到你。 - Victor
4个回答

7

这并不是关于读取通道的顺序,而是关于goroutine执行顺序的问题,其执行顺序并不保证。

尝试在写入通道的函数中使用“Println”(在写入前和写入后)并且我认为它应该与从通道中读取的顺序相同。


6
在Golang规范中,通道顺序被描述为:通道充当先进先出队列。例如,如果一个goroutine在通道上发送值,另一个goroutine接收它们,则这些值按发送顺序接收。它将输出哪个值首先可供其他端接收。如果您想要同步它们,请使用不同的通道或添加等待组。请注意保留HTML标记。
package main

import (
    "fmt"
)

func main() {
  messages1 := make(chan string)
  messages2 := make(chan string)
  go func(<-chan string) {
        messages2 <- "ping" 
    }(messages2)
  go func(<-chan string) {
        messages1 <- "hello" 
    }(messages1)
  fmt.Println(<-messages1)
  fmt.Println(<-messages2)
}

如果您看到,您可以使用不同的渠道轻松地按照您的选择获得任何想要的值。

Go playground


“它不取决于首先在通道上发送的值。它将打印哪个值首先可用于在另一端接收”是错误的。这些值总是按照它们被发送的顺序接收。“具有容量C的通道上的第k次接收发生在来自该通道的第k + C次发送完成之前。” https://golang.org/ref/mem#tmp_7 首先可用的值就是首先发送的值。 - Peter
好的,为什么先发送的值没有先打印出来呢?实际上我也想知道。因为我见过很多答案,每个人都说这并不重要。好的,谢谢,看到你给出的链接后,我也编辑了我的答案。 - Himanshu
@Himanshu,正如Alexander Trakhimenok在他的答案中所指出的那样,顺序取决于goroutine的调度和执行方式。据我所知,这是一种实现细节。您不应该期望您的goroutines按照您在源代码中编写它们的顺序执行。例如,go a(); go b(),这里Go规范并不保证a将在b之前运行,也许会,但也许不会。这取决于调度程序。如果b先运行,您将获得OP看到的结果。msg := <-messages将导致"ping"msg2 := <-messages将导致"hello" - mkopriva
是的,我知道这些事情都是关于Go协程的指责。但为什么OP会得到相同的答案呢?好吧,我已经给出了答案,以防OP想要控制哪个消息应该先打印。 - Himanshu

4

我刚刚经历了与你相同的问题。请查看我的帖子:Golang channels, order of execution

像你一样,我看到了一个反直觉的模式。在实际应该没有模式的地方。一旦启动了一个go进程,就启动了一个执行线程,而在这一点上,线程执行步骤的顺序基本上是不确定的。但如果有一个顺序,那么逻辑告诉我们第一个被调用的会首先执行。

实际上,如果每次重新编译该程序,结果都会有所不同。当我开始在本地计算机上编译/运行时,这就是我发现的情况。为了使结果随机化,我必须“污染”文件,例如添加和删除一个空格。然后编译器将重新编译程序,然后我将获得随机的执行顺序。但是在go沙箱中编译时,结果总是相同的。

当您使用沙箱时,结果似乎被缓存。我无法通过使用微不足道的更改来使结果在沙箱中更改。我唯一的方法是在启动go语句之间发布time.Sleep(1)命令。然后,每次启动的第一个语句将首先执行。但是,我仍然不认为我会把生命压在这方面,因为它们是单独的执行线程,并且没有任何保证。

底线是,我看到了一个应该没有确定性的确定性结果。这就是让我困惑的地方。当我发现在正常环境中结果实际上是随机的时,我完全明白了这一点。沙箱是一个非常好的工具。但它不是一个正常的环境。在本地编译和运行代码,您会看到您所期望的不同结果。


0

当我第一次遇到这个问题时感到困惑,但现在我已经清楚了。造成这种情况的原因不是通道,而是 goroutine。

正如Go 内存模型所提到的,不能保证 goroutine 的运行和退出,因此当您创建两个 goroutine 时,无法确保它们按顺序运行。

因此,如果您想要按照 FIFO 规则进行打印,则可以像这样更改代码:

func main() {
    messages := make(chan string)
    go func() {
        messages <- "hello"
        messages <- "ping"
    }()
    //go func() { messages <- "ping" }()
    msg := <-messages
    msg2 := <-messages
    fmt.Println(msg)
    fmt.Println(msg2)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接