【问题标题】:Golang concurrency: how to append to the same slice from different goroutinesGolang并发:如何从不同的goroutine附加到同一个切片
【发布时间】:2013-09-01 04:51:43
【问题描述】:

我有并发 goroutines 想要将一个(指向一个)结构​​的(指针)附加到同一个切片。 你如何在 Go 中编写它以使其并发安全?

这将是我的并发不安全代码,使用等待组:

var wg sync.WaitGroup
MySlice = make([]*MyStruct)
for _, param := range params {
    wg.Add(1)
    go func(param string) {
        defer wg.Done()
        OneOfMyStructs := getMyStruct(param)
        MySlice = append(MySlice, &OneOfMyStructs)
    }(param)
}
wg.Wait()

我猜你需要使用 go 通道来保证并发安全。任何人都可以举个例子吗?

【问题讨论】:

标签: concurrency go append goroutine


【解决方案1】:

频道是解决此问题的最佳方式。这是一个可以在 go playground 上运行的示例。

package main

import "fmt"
import "sync"
import "runtime"

type T int

func main() {
    var slice []T
    var wg sync.WaitGroup

    queue := make(chan T, 1)

    // Create our data and send it into the queue.
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            defer wg.Done()

            // Do stuff.
            runtime.Gosched()

            queue <- T(i)
        }(i)
    }

    // Poll the queue for data and append it to the slice.
    // Since this happens synchronously and in the same
    // goroutine/thread, this can be considered safe.
    go func() {
        defer wg.Done()
        for t := range queue {
            slice = append(slice, t)
        }
    }()

    // Wait for everything to finish.
    wg.Wait()

    fmt.Println(slice)
}

注意runtime.Gosched() 调用存在是因为这些 goroutine 不会屈服于调度程序。如果我们没有明确地做一些事情来触发所述调度程序,这将导致死锁。另一种选择可能是执行一些 I/O(例如:打印到标准输出)。但我发现runtime.Gosched() 的意图更简单、更清晰。

【讨论】:

  • 为什么channel接收goroutine需要调用defer wg.Done()?
  • 不必推迟。在这种情况下,只需在该 goroutine 结束时调用 wg.Done() 即可。当您有多个退出/返回时,延迟主要用于确保正确的行为。
  • 其实我的问题是为什么 wg.Done() 需要在第二个 go 例程中调用?第一个循环将清除 100 的计数器。
  • 原代码有个bug。你其实想要wg.Add(101)。您需要将其包含在等待组中,否则您可能会冒所有其他 goroutine 已完成的风险,但您在打印值之前尚未完成附加。
  • 这段代码有bug。 @ElgsQianChen wg.Done() 在第二个 go 例程中没有被调用。 @brendan 因为最后一个 wg.Done() 从未被调用过,将计数设置为 101 将导致死锁。
【解决方案2】:

使用sync.Mutex 保护MySlice = append(MySlice, &amp;OneOfMyStructs) 没有任何问题。但是当然你可以有一个缓冲区大小为len(params)的结果通道,所有goroutines都会发送他们的答案,一旦你的工作完成,你就会从这个结果通道中收集。

如果您的params 具有固定大小:

MySlice = make([]*MyStruct, len(params))
for i, param := range params {
    wg.Add(1)
    go func(i int, param string) {
         defer wg.Done()
         OneOfMyStructs := getMyStruct(param)
         MySlice[i] = &OneOfMyStructs
     }(i, param)
}

由于所有的 goroutine 都写入不同的内存,这并不奇怪。

【讨论】:

  • 您最后的考虑非常有趣:如果切片的大小已知并且您只是处理指向对象的指针,则根本不需要使用并发机制
  • 这不依赖于“指针切片”:它也适用于“MyStruct 切片”。同样,代码永远不会写入相同的内存。
  • 我假设指针的内存分配是固定的,而结构的内存分配是不固定的。那我想我错了。
  • 胡?什么是“固定”? Go 中的任何类型都有一定的内存布局,完全由编译时间决定。指针和其他东西没有区别。
  • 我了解到,除非有特殊需要,否则通常不应在通道中进行缓冲。对于您关于写入共享通道的 goroutines 的建议,您不应该只是同步交付(无缓冲)吗?
【解决方案3】:

@jimt 发布的答案不太正确,因为它错过了通道中发送的最后一个值,并且最后一个 defer wg.Done() 从未被调用。下面的 sn-p 有更正。

https://play.golang.org/p/7N4sxD-Bai

package main

import "fmt"
import "sync"

type T int

func main() {
    var slice []T
    var wg sync.WaitGroup

    queue := make(chan T, 1)

    // Create our data and send it into the queue.
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            // defer wg.Done()  <- will result in the last int to be missed in the receiving channel
            queue <- T(i)
        }(i)
    }

    go func() {
        // defer wg.Done() <- Never gets called since the 100 `Done()` calls are made above, resulting in the `Wait()` to continue on before this is executed
        for t := range queue {
            slice = append(slice, t)
            wg.Done()   // ** move the `Done()` call here
        }
    }()

    wg.Wait()

    // now prints off all 100 int values
    fmt.Println(slice)
}

【讨论】:

  • 来自未来:为什么@jimt 解决方案不起作用? wg.Done()deferred,所以只有在值通过通道发送后才会调用。我错过了什么?
  • @chris 我可以使用无缓冲通道吗? queue := make(chan T)
猜你喜欢
  • 2016-11-12
  • 2017-08-11
  • 2016-05-25
  • 2019-01-02
  • 2013-12-10
  • 2016-12-24
  • 2023-02-02
  • 2020-04-10
  • 2016-12-28
相关资源
最近更新 更多