Должны ли мы синхронизировать назначение переменных в goroutine?

#go #concurrency #synchronization #locking

#Вперед #параллелизм #синхронизация #блокировка

Вопрос:

Давайте предположим, что я объявил две карты и хочу назначить их в двух разных программах в группе ошибок. Я не выполняю никаких операций чтения / записи. Должен ли я защищать операцию назначения с помощью lock или я могу ее опустить?

UPD3: В части I, главе 3 Параллелизм Java на практике« Shared Objects Брайана Гетца упоминается:

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

 var (
    mu     sync.Mutex
    one    map[string]struct{}
    two    map[string]struct{}
)
g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
    resp, err := invokeFirstService(gctx, request)
    if err != nil {
        return err
    }
    mu.Lock()
    one = resp.One
    mu.Unlock()
    return nil
})

g.Go(func() error {
    resp, err := invokeSecondService(gctx, request)
    if err != nil {
        return err
    }
    mu.Lock()
    two = resp.Two
    mu.Unlock()
    return nil
})
if err := g.Wait(); err != nil {
    return err
}
// UPD3: added lock and unlock section
m.Lock()
defer m.Unlock()
performAction(one, two)
  

UPD: добавлен дополнительный контекст о переменных

UPD2: в чем были мои сомнения: у нас есть 3 программы — родительская и две в группе ошибок. нет гарантии, что разделяемая память нашей родительской goroutine получит последнее обновление после завершения работы errgroup goroutines, пока мы не установим доступ к разделяемой памяти с помощью барьеров памяти

Ответ №1:

Group.Wait() блокируется до тех пор, пока не вернутся все вызовы функций из Group.Go() метода, так что это точка синхронизации. Это гарантирует, что performAction(one, two) не начнется до того, как будут выполнены какие-либо записи в one и two , поэтому в вашем примере мьютекс не нужен.

 g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
    // ...
    one = resp.One
    return nil
})

g.Go(func() error {
    // ...
    two = resp.Two
    return nil
})

if err := g.Wait(); err != nil {
    return err
}
// Here you can access one and two safely:
performAction(one, two)
  

Если вы хотите получить доступ к one и two из других программ, в то время как программы, которые их записывают, выполняются одновременно, тогда да, вам нужно было бы заблокировать их, например:

 // This goroutine runs concurrently, so all concurrent access must be synchronized:
go func() {
    mu.Lock()
    fmt.Println(one, two)
    mu.Unlock()
}()

g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
    // ...
    mu.Lock()
    one = resp.One
    mu.Unlock()
    return nil
})

g.Go(func() error {
    // ...
    mu.Lock()
    two = resp.Two
    mu.Unlock()
    return nil
})

if err := g.Wait(); err != nil {
    return err
}
// Note that you don't need to lock here
// if the first concurrent goroutine only reads one and two.
performAction(one, two)
  

Также обратите внимание, что в приведенном выше примере вы могли бы использовать sync.RWMutex , и в goroutine, которая их считывает, RWMutex.RLock() и RWMutex.RUnlock() также было бы достаточно.

Комментарии:

1. в чем были мои сомнения: у нас есть 3 программы: родительская и две в группе ошибок. нет гарантии, что разделяемая память нашей родительской goroutine получит последнее обновление после завершения работы errgroup goroutines, пока мы не установим доступ к разделяемой памяти с помощью барьеров памяти

2. @SergiiGetman Если вы используете синхронизацию, у вас есть гарантия. sync.Mutex это один инструмент, как и Group or WaitGroup . Очевидно, что одного инструмента достаточно, вам не обязательно использовать несколько из них. Прочитайте Модель памяти Go для получения подробной информации.

3. Я обновил вопрос с добавлением блокировки / разблокировки при чтении в примере кода и замечанием о: reading and writing threads must synchronize on a common lock . Я понимаю, что это замечание связано с Java, но я полагаю, что оно применимо и к golang. Если это не связано, поправьте меня, пожалуйста

4. @SergiiGetman Это не применимо непосредственно к Go. В Go вам нужно установить связь «происходит до» . В коде вопроса существует такая взаимосвязь: g.Wait() вызов произойдет перед performAction() вызовом ( g.Wait() не вернется до тех пор, пока не будут запущены его программы), поэтому записи в программах гарантированно происходят и становятся видимыми перед performAction() вызовом, или, точнее, вычисляются его аргументы. Это происходит-before relationship дает вам гарантию, как описано в модели памяти Go (которую я предложил прочитать ранее).

5. большое спасибо за ваше объяснение! к сожалению, это все еще кажется мне сложным. В предоставленных вами ссылках есть глава Уничтожение Goroutine: Если эффекты goroutine должны наблюдаться другой goroutine, используйте механизм синхронизации, такой как блокировка или связь по каналу, чтобы установить относительный порядок. Вот почему я все еще в замешательстве.

Ответ №2:

В этом случае только одна goroutine может получить доступ к карте. Я не думаю, что вам нужна блокировка.