Приклад для sync.WaitGroup правильний?


108

Чи є в цьому прикладі sync.WaitGroupправильним використання ? Це дає очікуваний результат, але я не впевнений у wg.Add(4)своїх позиціях та позиціях wg.Done(). Чи має сенс додати відразу чотири гороутини wg.Add()?

http://play.golang.org/p/ecvYHiie0P

package main

import (
    "fmt"
    "sync"
    "time"
)

func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    duration := millisecs * time.Millisecond
    time.Sleep(duration)
    fmt.Println("Function in background, duration:", duration)
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(4)
    go dosomething(200, &wg)
    go dosomething(400, &wg)
    go dosomething(150, &wg)
    go dosomething(600, &wg)

    wg.Wait()
    fmt.Println("Done")
}

Результат (як очікувалося):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done

1
Що робити, якщо дозомеут () виходить з ладу, перш ніж це може зробити wg.Done ()?
Mostowski Згорнутись

19
Я усвідомлюю, що це по-старому, але майбутнім людям я би рекомендував початковий defer wg.Done()дзвінок на початку функціонування.
Брайан

Відповіді:


154

Так, цей приклад правильний. Важливо, щоб це wg.Add()сталося перед goзаявою, щоб запобігти умовам перегонів. Також було б правильним:

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go dosomething(200, &wg)
    wg.Add(1)
    go dosomething(400, &wg)
    wg.Add(1)
    go dosomething(150, &wg)
    wg.Add(1)
    go dosomething(600, &wg)

    wg.Wait()
    fmt.Println("Done")
}

Однак безглуздо передзвонювати wg.Addзнову і знову, коли ви вже знаєте, скільки разів він буде дзвонити .


Waitgroupsпанікувати, якщо лічильник опуститься нижче нуля. Лічильник починається з нуля, кожен Done()- a, -1і кожен Add()залежить від параметра. Таким чином, щоб гарантувати , що лічильник ніколи не опускається нижче і уникнути панікує, вам потрібно Add()буде гарантовано прийти до Done().

У Go такі такі гарантії дає модель пам'яті .

Модель пам’яті зазначає, що всі оператори в одній goroutі, як видається, виконуються в тому ж порядку, як вони написані. Цілком можливо, що вони насправді не будуть в такому порядку, але результат буде таким, як він був. Також гарантується, що програма не запускається до завершення goоператора, який викликає її . Оскільки Add()відбувається перед goтвердженням і goтвердження відбувається перед Done(), ми знаємо, що Add()відбувається перед Done().

Якщо ви повинні мати goзаяву, перш ніж програма Add(), програма може працювати правильно. Однак це було б умовою гонки, оскільки це не було б гарантовано.


9
У мене є питання щодо цього: чи не було б краще, щоб defer wg.Done()ми були впевнені, що він називається незалежно від маршруту, який проходить програма? Дякую.
Алессандро Сантіні

2
якщо ви просто хотіли переконатися, що функція не повертається до того, як будуть завершені всі підпрограми йти, тоді перевагу буде віддалено. Зазвичай вся справа групи очікування - чекати, поки буде виконана вся робота, щоб потім зробити щось з результатами, яких ви чекали.
Занвен

1
Якщо ви не користуєтесь, deferі одна з ваших програм не зателефонувала wg.Done()... чи Waitпросто не блокується назавжди? Це здається, що це міг легко ввести важко знайти помилку у ваш код ...
Dan Esparza

29

Я рекомендую вбудувати wg.Add()виклик у саму doSomething()функцію, так що якщо ви регулюєте кількість разів, коли вона викликається, вам не доведеться окремо коригувати параметр додавання вручну, що може призвести до помилки, якщо ви оновите його, але забудьте оновити оновлення інше (у цьому тривіальному прикладі малоймовірно, але все ж я особисто вважаю, що це краща практика для повторного використання коду).

Як вказує Стівен Вайнберг у своїй відповіді на це запитання , вам доведеться збільшувати групу очікування перед нерестуванням функціоналу, але це можна легко досягти, загорнувши нерест gofunc всередину самої doSomething()функції, як це:

func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    wg.Add(1)
    go func() {
        duration := millisecs * time.Millisecond
        time.Sleep(duration)
        fmt.Println("Function in background, duration:", duration)
        wg.Done()
    }()
}

Тоді ви можете телефонувати без goвиклику, наприклад:

func main() {
    var wg sync.WaitGroup
    dosomething(200, &wg)
    dosomething(400, &wg)
    dosomething(150, &wg)
    dosomething(600, &wg)
    wg.Wait()
    fmt.Println("Done")
}

Як ігровий майданчик: http://play.golang.org/p/WZcprjpHa_


21
  • невелике поліпшення на основі відповіді Мрот
  • використання відстрочки для Done є безпечнішим
  func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
  wg.Add(1)
  go func() {
      defer wg.Done()
      duration := millisecs * time.Millisecond
      time.Sleep(duration)
      fmt.Println("Function in background, duration:", duration)
  }()
}

func main() {
  var wg sync.WaitGroup
  dosomething(200, &wg)
  dosomething(400, &wg)
  dosomething(150, &wg)
  dosomething(600, &wg)
  wg.Wait()
  fmt.Println("Done")
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.