Могу ли я одновременно писать разные элементы среза

У меня есть фрагмент, который содержит работу, которая будет выполнена, и срез, который будет содержать результаты, когда все будет сделано. Ниже приводится схема моего общего процесса:

var results = make([]Result, len(jobs))
wg := sync.WaitGroup{}
for i, job := range jobs {
    wg.Add(1)
    go func(i int, j job) {
        defer wg.Done()
        var r Result = doWork(j)
        results[i] = r
    }(i, job)
}
wg.Wait()
// Use results

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

Я полагаю, что альтернатива собирает результаты через канал, но поскольку порядок результатов имеет значение, это казалось довольно простым. Безопасно ли писать элементы среза таким образом?

Ответ 1

Правило простое: если несколько goroutines получают доступ к переменной одновременно, и по крайней мере один из этих запросов является записью, тогда требуется синхронизация.

Ваш пример не нарушает это правило. Вы не записываете значение среза (заголовок среза), вы его читаете (неявно, когда вы его индексируете).

Вы не читаете элементы среза, вы только изменяете элементы среза. И каждый goroutine только модифицирует один, другой, назначенный элемент среза. И поскольку каждый элемент среза имеет свой собственный адрес (собственное пространство памяти), они похожи на разные переменные. Это описано в Spec: Variables:

Структурированные переменные типов массива, среза и структуры имеют элементы и поля, которые могут быть рассмотрены индивидуально. Каждый такой элемент действует как переменная.

Следует иметь в виду, что вы не можете читать результаты из среза results без синхронизации. И waitgroup, которую вы использовали в вашем примере, является достаточной синхронизацией. Вы можете прочитать срез, как только wg.Wait() вернется, потому что это может произойти только после того, как все рабочие wg.Done() называются wg.Done(), и ни один из рабочих wg.Done() элементы после того, как они вызвали wg.Done().

Например, это действительный (безопасный) способ проверки/обработки результатов:

wg.Wait()
// Safe to read results after the above synchronization point:
fmt.Println(results)

Но если вы попытаетесь получить доступ к элементам results перед wg.Wait(), то гонка данных:

// This is data race! Goroutines might still run and modify elements of results!
fmt.Println(results)
wg.Wait()

Ответ 2

Да, это совершенно законно: срез имеет массив в качестве основного хранилища данных, и, будучи составным типом, массив представляет собой последовательность "элементов", которые ведут себя как отдельные переменные с различными ячейками памяти; их одновременная модификация в порядке.

Просто не забудьте синхронизировать завершение работы ваших рабочих goroutines с основным, прежде чем читать обновленное содержимое среза.

Использование sync.WaitGroup для этого - как и вы, - отлично.

Кроме того, как сказал @icza, вы не должны изменять значение самого среза (которое представляет собой структуру, содержащую указатель на массив хранилища, емкость и длину).