синхронизация.Инициализация группы ожидания перед запуском goroutine

# #go #synchronization #race-condition #goroutine #waitgroup

Вопрос:

У меня был следующий код в рамках теста:

     expected := 10
    var wg sync.WaitGroup
    for i := 0; i < expected; i   {
        go func(wg *sync.WaitGroup) {
            wg.Add(1)
            defer wg.Done()
            // do something
        }(amp;wg)
    }
    wg.Wait()
 

К моему удивлению, я получил panic: Fail in goroutine after TestReadWrite has completed при запуске «go test». Когда я бежал с «go test-race», у меня не было паники, но позже тест провалился. В обоих случаях, несмотря на наличие wg.Wait(), подпрограмма не завершила выполнение.

Я внес следующее изменение, и теперь тест работает так, как ожидалось:

     expected := 10
    var wg sync.WaitGroup
    wg.Add(expected)
    for i := 0; i < expected; i   {
        go func(wg *sync.WaitGroup) {
            defer wg.Done()
            // do something
        }(amp;wg)
    }
    wg.Wait()
 

Мои сомнения таковы:

  1. Большая часть кода, который я видел до сих пор, выполняется wg.Add(1) внутри горутины. Почему он ведет себя неожиданно в данном конкретном случае? Похоже, что здесь происходит то, что некоторые гороутины, похоже, заканчивают работу и проходят wg.Wait (), прежде чем другие гороутины даже начнут работать. Является ли использование wg.Add(1) внутри goroutine опасным / которого следует избегать? Если это не проблема в целом, то в чем именно заключается проблема здесь?
  2. Является ли добавление wg.Add(expected) правильным способом решения этой проблемы?

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

1. Ваш второй подход совершенно хорош. Однако вы можете продолжать использовать wg.Add(1) его, если переместите его чуть выше гороутины, т. Е. Держите его внутри цикла, но вне гороутины.

2. Большая часть кода, который я видел до сих пор, выполняется wg.Add(1) внутри горутины — похоже, вы видите много плохого кода: если вы делаете это внутри той же горутины, которая была только что добавлена, часто бывает слишком поздно. (Это может быть нормально при условии , что есть какая-то причина, по которой эта подпрограмма должна быть запущена и запущена до момента Add завершения, прежде чем кто-либо за пределами этой подпрограммы позвонит wg.Wait .)

3. Спасибо — я просто перечитал код и заметил, что это была моя ошибка-все образцы действительно выполняли wg. Добавьте вне горотины.

Ответ №1:

Ваш первый подход вызывает панику, потому что (WaitGroup.Add):

Add добавляет дельту, которая может быть отрицательной, к счетчику групп ожидания. Если счетчик становится равным нулю, все карты, заблокированные во время ожидания, освобождаются. Если счетчик станет отрицательным, добавьте панику.

Обычно это означает, что вызовы для добавления должны выполняться до оператора, создающего подпрограмму или другое событие, которого следует ожидать

Когда в конце кода вызывается Wait (), ни одна из горизонталей, возможно, еще не начала выполняться — следовательно, значение, хранящееся в группе ожидания, равно 0. Затем, позже, когда ваша подпрограмма go будет выполнена, вызывающая подпрограмма go уже выпущена. Это приведет к неожиданному поведению, панике в вашем случае. Может быть, вы использовали значения из вызывающей процедуры go-там.

Ваш второй подход абсолютно хорош. Вы также можете звонить .Add(1) внутри цикла, но за пределами go func блока

Ответ №2:

Согласно документам

Группа ожидания ожидает завершения набора горотинов. Основные вызовы goroutine Добавляют, чтобы установить количество гороутин для ожидания. Затем каждый из goroutines запускается и вызывает Done, когда закончит. В то же время, ожидание может быть использовано для блокировки до тех пор, пока все городские линии не будут завершены.

Так Add() должен быть вызван гороутином, который инициирует другие гороутины, которые в вашем случае являются main гороутинами.

В первом фрагменте кода вы вызываете Add() внутри других подпрограмм вместо основной подпрограммы, которая вызывает проблему —

 expected := 10
var wg sync.WaitGroup
for i := 0; i < expected; i   {
   go func(wg *sync.WaitGroup) {
       wg.Add(1) // Do not call Add() here
       defer wg.Done()
       // do something
   }(amp;wg)
}
wg.Wait()
 

Второй фрагмент работает, потому что вы вызываете Add() в main goroutine —

 expected := 10
var wg sync.WaitGroup
wg.Add(expected) // Okay
for i := 0; i < expected; i   {
    go func(wg *sync.WaitGroup) {
       defer wg.Done()
       // do something
     }(amp;wg)
}
wg.Wait()
 

Является ли добавление wg.Add(ожидаемое) правильным способом решения этой проблемы?

Вы также можете вызвать wg.Add(1) цикл for —

 expected := 10
var wg sync.WaitGroup
for i := 0; i < expected; i   {
    wg.Add(1) // Okay
    go func(wg *sync.WaitGroup) {
       defer wg.Done()
       // do something
     }(amp;wg)
}
wg.Wait()