试图理解 goroutines

Trying to understand goroutines

我一直在研究 A Tour of Go 中的以下代码,但是当我应用一些小的更改时,我不明白发生了什么。原代码是这样的

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

它产生这个

world
hello
hello
world
world
hello
hello
world
world
hello

这还行:五次你好,五次世界。当我打电话时我开始变得奇怪

say("world")
go say("hello")

现在输出只是

world
world
world
world
world

没有任何问候。有两个 goroutines

就更奇怪了
go say("world")
go say("hello")

现在完全没有输出。当我将 i < 5 更改为 i < 2 并调用

go say("world")
say("hello")

我明白了

world
hello
hello

我在这里错过了什么?

因为main函数已经退出

main函数return时,所有goroutine突然终止,然后程序退出。

您添加一条语句:

time.Sleep(100 * time.Second)

在主函数return之前,一切顺利。

但是在 Go 中一个好的做法是使用 channel,它用于 goroutine 之间的通信。您可以使用它让 main 函数等待后台 goroutines 完成。

的情况下
 say("world")
 go say("hello")

"world" 调用必须在 "hello" goroutine 启动之前完成。 "hello" goroutine 没有 运行 或完成 because main returns.

对于

go say("world")
go say("hello")

goroutines 没有 运行 或完成,因为 main returns.

使用 sync.WaitGroup 来防止 main 在 goroutines 完成之前退出:

func say(wg *sync.WaitGroup, s string) {
  defer wg.Done()
  for i := 0; i < 5; i++ {
    time.Sleep(100 * time.Millisecond)
    fmt.Println(s)
  }
}

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go say(&wg, "world")
  go say(&wg, "hello")
  wg.Wait()
}

playground example

func main() { go say("world") say("hello") }

您正在创建两个独立的 goroutine,一个是 main functions goroutine,一个是 go say("world")。通常当执行函数时,程序会跳转到该函数,执行其中的所有代码,然后跳转到调用函数的位置之后的行。

对于 goroutine,您不会跳转到函数内部,而是在单独的线程中启动 goroutine,并在调用之后继续执行该行,而无需等待它。

因此,在主协程完成之前,协程将没有时间完成。

恭喜你学习围棋。作为新手,很高兴了解并发性以及它与并行性的区别。

并发
并发就像杂耍演员用一只手在空中玩杂耍几个球。无论他在杂耍多少个球,任何时候都只有一个球碰到他的手。

平行度
当变戏法者开始用另一只手同时玩更多的球时,我们同时有两个并发的 进程 运行。

Goroutines 很棒,因为它们都是 并发自动并行,这取决于可用的计算核心和 GOMAXPROCS 正在设置变量。

单手杂耍者
回到单手、单核、并发的变戏法。想象一下,他玩着三个分别名为 "hello"、"world" 和 "mars" 的球,手是 main 套路。

var balls = []string{"hello", "world", "mars"}

func main() {
        go say(balls[0])
        go say(balls[1])
        go say(balls[2])
}

或者更恰当地说,

func main() {
        for _, ball := range balls {
                go say(ball)
        }
}

一旦三个球依次抛向空中,变戏法者立即收回手。也就是说,main 例程在第一个球落在他的手上之前就退出了。耻辱,球只是掉到地上。糟糕的表现。

为了让球回到他的手中,变戏法者必须确保他等待。这意味着他的手需要能够跟踪和计算他投掷的球并了解每个球何时落地。

最直接的方法是使用sync.WaitGroup:

import (
    "fmt"
    "time"
    "sync"
)

var balls = []string{"hello", "world", "mars"}
var wg sync.WaitGroup

func main() {
        for _, ball := range balls {
                // One ball thrown
                wg.Add(1)
                go func(b string) {
                        // Signals the group that this routine is done.
                        defer wg.Done()
                        // each ball hovers for 1 second
                        time.Sleep(time.Duration(1) * time.Second)
                        fmt.Println(b)
                        // wg.Done() is called before goroutine exits
                }(ball)
        }

        // The juggler can do whatever he pleases while the 
        // balls are hanging in the air.

        // This hand will come back to grab the balls after 1s.
        wg.Wait()
}

WaitGroup 很简单。当生成一个 goroutine 时,将 WaitGroup.Add(1) 添加到 "backlog counter" 并调用 WaitGroup.Done() 来减少计数器。一旦 backlog 变为 0,就意味着所有的 goroutines 都完成了,WaitGroup 应该停止等待(抓住球!)。

虽然使用通道进行同步很好,但我们鼓励您酌情使用可用的并发工具,尤其是当通道的使用使代码更加复杂且难以理解时。