如何使用sync/errgroup包在Go中编写并发for循环

问题描述 投票:0回答:2

我想同时对切片的元素执行操作
我正在使用 sync/errgroup 包来处理并发

这是 Go Playground 上的最小复制品 https://go.dev/play/p/yBCiy8UW_80

import (
    "fmt"
    "golang.org/x/sync/errgroup"
)

func main() {
    eg := errgroup.Group{}
    input := []int{0, 1, 2}
    output1 := []int{}
    output2 := make([]int, len(input))
    for i, n := range input {
        eg.Go(func() (err error) {
            output1 = append(output1, n+1)
            output2[i] = n + 1
            return nil
        })
    }
    eg.Wait()
    fmt.Printf("with append %+v", output1)
    fmt.Println()
    fmt.Printf("with make %+v", output2)
}

输出

with append [3 3 3]
with make [0 0 3]

与预期相比

[1 2 3]

go concurrency slice goroutine
2个回答
6
投票

这里有两个不同的问题:


首先,循环中的变量在每个 goroutine 有机会读取它们之前就已经发生了变化。当你有一个像

这样的循环时
for i, n, := range input {
  // ...
}

变量

i
n
在整个循环期间都有效。当控制到达循环底部并跳回顶部时,这些变量将被分配新值。如果循环中启动的 goroutine 正在使用这些变量,那么它们的值将发生不可预测的变化。这就是为什么您会看到相同的数字在示例的输出中多次出现。在第一次循环迭代中启动的 goroutine 在
n
已设置为 2 之前不会开始执行。

要解决此问题,您可以执行 NotX 的答案所示的操作,并创建范围仅限于循环的单次迭代的新变量:

for i, n := range input {
  ic, nc := i, n
  // use ic and nc instead of i and n
}

循环内声明的变量的作用域仅限于循环的一次迭代,因此当循环的下一次迭代开始时,会创建全新的变量,从而防止原始变量在 goroutine 启动时和实际开始运行之间发生变化.

更新:从 Go 1.22 开始,这不再是问题,因为循环变量已更改为具有每次迭代范围。您可以在发行说明中查看详细信息。


其次,您同时从不同的 goroutine 修改相同的值,这是不安全的。特别是,您使用

append
同时附加到同一切片。在这种情况下发生的事情是不确定的,各种糟糕的事情都可能发生。

有两种方法可以解决这个问题。您已经设置的第一个:使用

make
预先分配一个输出切片,然后让每个 goroutine 填充切片中的特定位置:

output := make([]int, 3)
for i, n := range input {
  ic, nc := i, n
  eg.Go(func() (err error) {
    output[ic] = nc + 1
    return nil
  })
}
eg.Wait()

如果您知道启动循环时将有多少个输出,这会非常有用。

另一种选择是使用某种锁定来控制对输出切片的访问。

sync.Mutex
非常适合这个:

var output []int
mu sync.Mutex
for _, n := range input {
  nc := n
  eg.Go(func() (err error) {
    mu.Lock()
    defer mu.Unlock()
    output = append(output, nc+1)
    return nil
  })
}
eg.Wait()

如果您不知道有多少个输出,则此方法有效,但它不能保证有关输出顺序的任何信息 - 它可以是任何顺序。如果你想把它按顺序排列,你总是可以在所有 goroutine 完成后进行某种排序。


2
投票

运行某些 Go 例程时,无法保证顺序。因此,虽然可以预期元素

1
2
3
,但您不应该对顺序做出任何假设。

无论如何,看起来第一个

eg.Go()
调用是在 for
for
循环实际上到达其第三个元素时发生的。这就是为什么您只能获得
3
,并且只能在第三个位置(其中
i=2
)进行索引访问。

如果你像这样复制你的值,问题就得到了一定程度的解决:

for i, n := range input {
    nc, ic := n, i
    eg.Go(func() (err error) {
        output1 = append(output1, nc+1)
        output2[ic] = nc + 1
        return nil
    })
}

也就是说,结果看起来像这样

with append [3 2 1]
with make [1 2 3]

对我来说,所以订单仍然不是我们预期的。 不过,我不是

errgroup
包方面的专家,所以也许其他人可以分享有关执行顺序的更多信息。

© www.soinside.com 2019 - 2024. All rights reserved.