Contoh untuk sync.WaitGroup benar?

108

Apakah penggunaan contoh ini sync.WaitGroupbenar? Ini memberikan hasil yang diharapkan, tetapi saya tidak yakin tentang wg.Add(4)dan posisi dari wg.Done(). Apakah masuk akal untuk menambahkan empat goroutine sekaligus 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")
}

Hasil (seperti yang diharapkan):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done
topskip
sumber
1
Bagaimana jika dosomething () crash sebelum dapat melakukan wg.Done ()?
Mostowski Runtuh
19
Saya menyadari ini sudah lama, tetapi untuk orang masa depan, saya akan merekomendasikan defer wg.Done()panggilan awal di awal fungsi.
Brian

Jawaban:

154

Ya, contoh ini benar. Penting agar wg.Add()terjadi sebelum gopernyataan untuk mencegah kondisi balapan. Hal berikut juga akan benar:

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")
}

Namun, tidak ada gunanya menelepon wg.Addberulang kali ketika Anda sudah tahu berapa kali akan dipanggil.


Waitgroupspanik jika penghitung jatuh di bawah nol. Penghitung dimulai dari nol, masing Done()- masing adalah a -1dan masing-masing Add()bergantung pada parameter. Jadi, untuk memastikan bahwa meja tidak pernah turun di bawah dan panik menghindari, Anda perlu Add()untuk dijamin untuk datang sebelum Done().

Dalam Go, jaminan seperti itu diberikan oleh model memori .

Model memori menyatakan bahwa semua pernyataan dalam satu goroutine tampaknya dijalankan dalam urutan yang sama seperti saat ditulis. Mungkin saja mereka tidak benar-benar berada dalam urutan itu, tetapi hasilnya akan seperti semula. Juga dijamin bahwa goroutine tidak akan berjalan hingga gopernyataan yang memanggilnya . Karena Add()terjadi sebelum gopernyataan dan gopernyataan muncul sebelum Done(), kita tahu Add()terjadi sebelum Done().

Jika Anda mendapatkan gopernyataan itu sebelum Add(), program dapat beroperasi dengan benar. Namun, itu akan menjadi kondisi balapan karena tidak ada jaminan.

Stephen Weinberg
sumber
9
Saya punya pertanyaan tentang yang satu ini: bukankah lebih baik defer wg.Done()jika kita yakin itu dipanggil terlepas dari rute yang diambil goroutine? Terima kasih.
Alessandro Santini
2
jika Anda benar-benar ingin memastikan bahwa fungsi tersebut tidak kembali sebelum semua rutinitas go selesai maka yes defer akan lebih disukai. hanya biasanya inti dari kelompok tunggu adalah menunggu sampai semua pekerjaan selesai untuk kemudian melakukan sesuatu dengan hasil yang Anda tunggu.
Zanven
1
Jika Anda tidak menggunakan deferdan salah satu goroutine Anda gagal memanggil wg.Done()... bukankah Anda akan Waitmemblokir selamanya? Sepertinya ini dapat dengan mudah memasukkan bug yang sulit ditemukan ke dalam kode Anda ...
Dan Esparza
29

Saya akan merekomendasikan menyematkan wg.Add()panggilan ke dalam doSomething()fungsi itu sendiri, sehingga jika Anda menyesuaikan berapa kali dipanggil, Anda tidak perlu secara terpisah menyesuaikan parameter tambah secara manual yang dapat menyebabkan kesalahan jika Anda memperbaruinya tetapi lupa untuk memperbarui lainnya (dalam contoh sepele ini yang tidak mungkin, tetapi tetap saja, saya pribadi percaya ini adalah praktik yang lebih baik untuk penggunaan ulang kode).

Seperti yang ditunjukkan oleh Stephen Weinberg dalam jawabannya untuk pertanyaan ini , Anda memang harus menaikkan grup tunggu sebelum memijahkan gofunc, tetapi Anda dapat melakukannya dengan mudah dengan membungkus bibit gofunc di dalam doSomething()fungsi itu sendiri, seperti ini:

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()
    }()
}

Kemudian Anda dapat memanggilnya tanpa gopermintaan, misalnya:

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

Sebagai taman bermain: http://play.golang.org/p/WZcprjpHa_

mroth
sumber
21
  • peningkatan kecil berdasarkan jawaban Mroth
  • menggunakan penundaan karena Selesai lebih aman
  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")
}
Bnaya
sumber