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


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

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
Это, кажется, работает, но я не проверил его тщательно и не уверен, безопасно ли это делать. Вообще-то я не хотел бы позволять нескольким goroutines писать в что-либо, но в этом случае каждая goroutine ограничена своим собственным индексом в срезе, который предварительно выделен.

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

2 11

2 ответа:

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

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

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

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

Что должно быть сохранено имейте в виду, что вы не можете прочитать результаты из среза results без синхронизации. И группа ожидания, которую вы использовали в своем примере, является достаточной синхронизацией. Вы можете прочитать срез после того, как wg.Wait() возвратится, потому что это может произойти только после того, как все рабочие горотины будут вызваны 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()

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

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

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

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