首页 > 解决方案 > 带有通道的 Goroutines 的行为

问题描述

下面给出的代码输出有些令人困惑,请帮助我了解通道和 goroutine 的行为以及实际执行是如何发生的。

我试图理解程序的流程,但是“调用goroutine”之后的语句被执行,即使调用了goroutine,稍后也会执行goroutines中的语句,

在第二个“goroutine 调用”中,行为不同,并且程序的打印/流程顺序发生了变化。

以下是代码:

    package main

    import "fmt"

    func main() {
        fmt.Println("1")
        done := make(chan string)
        go test(done)
        fmt.Println("7")
        fmt.Println(<-done)
        fmt.Println("8")
        fmt.Println(<-done)
        fmt.Println("9")
        fmt.Println(<-done)
    }
    func test(done chan string) {
        fmt.Println("2")
        done <- "3"
        done <- "10"
        fmt.Println("4")
        done <- "5"
       fmt.Println("6")
    }

上述代码的结果:

1
7
2
3
8
10
9
4
6
5

请帮助我理解为什么以及如何得出这个结果。

标签: gochannelgoroutinechannels

解决方案


欢迎来到堆栈溢出。我希望我能帮助你更好地理解通道和 goroutines。

概念 1:渠道

将通道可视化为管道,数据从一端进入另一端。输入的第一个数据是从另一侧出来的第一个数据。有缓冲通道和非缓冲通道,但对于您的示例,您只需要了解默认通道,它是无缓冲的。无缓冲通道一次只允许通道中有一个值。

写入无缓冲通道

看起来像这样的代码将数据写入通道的一端。

ch <- value

现在,这段代码实际上等待执行,直到有东西从通道中读取值。无缓冲通道一次只允许一个值在其中,并且在读取之前不会继续执行。稍后我们将看到这如何影响代码执行的顺序。

从无缓冲通道读取

要从无缓冲通道读取(可视化从通道中取出一个值),执行此操作的代码如下所示

[value :=] <-ch

当您阅读代码文档时,[things in] 方括号表示其中的内容是可选的。上面,如果没有 [value :=],您只会从通道中取出一个值,而不会将它用于任何事情。

现在,当通道中有值时,此代码有两个副作用。第一,它在我们现在所处的任何例程中从通道中读取值,然后继续处理该值。它的另一个作用是允许将值放入通道的 goroutine 继续。这是理解您的示例程序所必需的关键部分。

如果通道中还没有值,它将等待一个值写入通道,然后再继续。换句话说,线程会阻塞,直到通道有要读取的值。

概念 2:Goroutines

goroutine 允许您的代码继续同时执行两段代码。这可以用来让你的代码执行得更快,或者同时处理多个问题(想想一个服务器,多个用户同时从它加载页面)。

当您尝试找出在同时执行多个例程时执行代码的顺序时,就会出现您的问题。这是一个很好的问题,其他人已经正确地指出这取决于。当您生成两个 goroutine 时,执行哪些代码行的顺序是任意的。

下面带有 goroutine 的代码可以打印executing a()end main()先打印。这是因为产生一个 gorouting 意味着有两个并发的执行流(线程)同时发生。在这种情况下,一个线程留在main()其中,另一个开始执行a(). 运行时如何决定先运行哪个是任意的。

func main() {
    fmt.Println("start main()")
    go a()
    fmt.Println("end main()")
}

func a() {
    fmt.Println("executing a()")
}

Goroutines + 通道

现在让我们使用一个通道来控制什么时候执行什么的顺序。现在唯一的区别是我们创建了一个通道,将它传递给 goroutine,然后等待它的值被写入,然后再继续 main。从前面开始,我们讨论了从通道读取值的例程如何需要等到通道中有值才能继续。由于executing a()总是在写入通道之前打印,我们将始终等待读取放入通道的值,直到executing a()打印。由于我们在打印之前从通道读取(发生在通道写入之后)end main(),所以executing a()总是会在打印之前打印end main()。我制作了这个游乐场,所以你可以自己运行它。

func main() {
    fmt.Println("start main()")
    ch := make(chan int)
    go a(ch)
    <-ch
    fmt.Println("end main()")
}

func a(ch chan int) {
    fmt.Println("executing a()")
    ch <- 0
}

你的例子

我认为在这一点上你可以弄清楚什么时候会发生什么,以及可能以不同的顺序发生什么。当我在脑海中经历它时,我自己的第一次尝试是错误的(参见编辑历史)。你必须要小心!在编辑时我不会给出正确的答案,因为我意识到这可能是一项家庭作业。

编辑:关于更多语义<-done

在我第一次经历时,我忘了提到它fmt.Println(<-done)在概念上与以下内容相同。

value := <-done
fmt.Println(value)

这很重要,因为它可以帮助您看到当main()线程从done通道读取时,它不会同时打印它。这是运行时的两个独立步骤。

我希望这有帮助。如果您有任何问题,请告诉我。


推荐阅读