Параллелизм голанга: как добавить к одному и тому же срезу из разных горутин

У меня есть параллельные горутины, которые хотят добавить структуру (указатель на) к одному и тому же фрагменту. Как написать это на 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 для обеспечения безопасности параллелизма. Может ли кто-нибудь поделиться примером?


person Daniele B    schedule 28.08.2013    source источник
comment
Я считаю, что ответ здесь хорошо отвечает на этот вопрос: stackoverflow.com/questions/18467445/   -  person Gustavo Niemeyer    schedule 29.08.2013


Ответы (3)


Нет ничего плохого в защите MySlice = append(MySlice, &OneOfMyStructs) с помощью sync.Mutex. Но, конечно, у вас может быть канал результатов с размером буфера len(params), все горутины отправляют свои ответы, и как только ваша работа будет завершена, вы собираете данные из этого канала результатов.

Если ваш 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)
}

Поскольку все горутины пишут в разную память, это не очень весело.

person Volker    schedule 28.08.2013
comment
Очень интересно ваше последнее соображение: в случае, если размер среза известен, и вы просто имеете дело с указателями на объекты, вам вообще не нужно использовать механизм параллелизма. - person Daniele B; 29.08.2013
comment
Это не зависит от фрагмента указателей: это будет работать также для фрагмента MyStruct. Опять же, код никогда не записывается в одну и ту же память. - person Volker; 29.08.2013
comment
Я предполагал, что выделение памяти для указателя фиксировано, а выделение памяти для структуры не фиксировано. Тогда я полагаю, что ошибаюсь. - person Daniele B; 29.08.2013
comment
Ху? Что исправлено? Любой тип в Go имеет определенную структуру памяти, которая полностью определяется временем компиляции. Никакой разницы между указателем и чем-то еще. - person Volker; 29.08.2013
comment
Я читал, что обычно у вас не должно быть буферизации в каналах, если нет особой необходимости. Что касается вашего предложения о записи горутин в общий канал, разве это не тот случай, когда вы просто доставляете синхронно (без буферизации)? - person Bjarke Ebert; 26.02.2016

Ответ, отправленный @jimt, не совсем правильный, поскольку он пропускает последнее значение, отправленное в канале, и последнее defer wg.Done() никогда не вызывается. В приведенном ниже фрагменте есть исправления.

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)
}
person chris    schedule 21.08.2016
comment
Из будущего: почему решение @jimt не работает? wg.Done() deferred, поэтому он вызывается только после того, как значение отправлено по каналу. Что мне не хватает? - person ineiti; 02.03.2021
comment
@chris Могу ли я использовать небуферизованный канал? queue := make(chan T) - person Pioz; 16.03.2021

Канал - лучший способ решить эту проблему. Вот пример, который можно запустить на игровой площадке.

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() существует потому, что эти горутины не подчиняются планировщику. Что приведет к тупиковой ситуации, если мы явно не сделаем что-то для запуска указанного планировщика. Другой вариант мог заключаться в выполнении некоторого ввода-вывода (например, печать в стандартный вывод). Но я считаю, что цель runtime.Gosched() проще и яснее.

person jimt    schedule 28.08.2013
comment
Почему каналу, принимающему горутину, нужно вызывать функцию defer wg.Done ()? - person Qian Chen; 18.04.2015
comment
Это не нужно откладывать. В этом случае будет работать только wg.Done() вызов в конце этой горутины. Отсрочка в основном полезна для обеспечения правильного поведения, когда у вас есть несколько выходов / возвратов. - person jimt; 19.04.2015
comment
На самом деле мой вопрос заключался в том, почему wg.Done() нужно вызывать во второй процедуре? Первый цикл очистит счетчик до 100. - person Qian Chen; 19.04.2015
comment
В исходном коде есть ошибка. Вам действительно нужен wg.Add (101). Вам нужно включить его в группу ожидания, иначе вы рискуете, что все другие горутины завершились, но вы не завершили добавление до того, как напечатаете значения. - person brendan; 23.11.2015
comment
В этом коде есть ошибки. @ElgsQianChen wg.Done() не вызывается во второй подпрограмме. @brendan, поскольку последний wg.Done() никогда не вызывается, установка счетчика на 101 приведет к тупиковой ситуации. - person chris; 21.08.2016
comment
@chris есть ли что-нибудь исправить? - person shanab; 22.11.2016