ตัวอย่างสำหรับ sync.WaitGroup ถูกต้องหรือไม่


108

ตัวอย่างการใช้งานนี้sync.WaitGroupถูกต้องหรือไม่? มันให้ผลลัพธ์ที่คาดหวัง แต่ฉันไม่แน่ใจเกี่ยวกับตำแหน่งwg.Add(4)และตำแหน่งของwg.Done(). การเพิ่ม goroutines สี่ตัวพร้อมกันนั้นสมเหตุสมผล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
จะเกิดอะไรขึ้นถ้า dosomething () ขัดข้องก่อนที่จะสามารถทำ wg.Done ()?
Mostowski ยุบ

19
ฉันรู้ว่านี่เป็นเรื่องเก่า แต่สำหรับคนในอนาคตฉันขอแนะนำให้defer wg.Done()โทรครั้งแรกเมื่อเริ่มฟังก์ชัน
Brian

คำตอบ:


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()เวลา

ไปในการค้ำประกันดังกล่าวจะได้รับจากหน่วยความจำแบบ

โมเดลหน่วยความจำระบุว่าคำสั่งทั้งหมดใน goroutine เดียวดูเหมือนจะดำเนินการตามลำดับเดียวกันกับที่เขียน เป็นไปได้ว่าพวกเขาจะไม่อยู่ในลำดับนั้นจริง ๆ แต่ผลลัพธ์จะเป็นไปอย่างนั้น นอกจากนี้ยังรับประกันว่าgoroutine ไม่ทำงานจนกว่าจะได้goคำสั่งที่เรียกมันว่า เนื่องจากAdd()เกิดขึ้นก่อนgoคำสั่งและgoคำสั่งเกิดขึ้นก่อนคำสั่งDone()เราจึงทราบว่าAdd()เกิดขึ้นก่อนที่Done().

หากคุณต้องมีgoคำสั่งมาก่อนAdd()โปรแกรมอาจทำงานได้อย่างถูกต้อง อย่างไรก็ตามมันจะเป็นเงื่อนไขการแข่งขันเพราะมันจะไม่รับประกัน


9
ฉันมีคำถามเกี่ยวกับคำถามนี้: มันจะดีกว่าdefer wg.Done()ไหมที่เราจะแน่ใจว่ามันถูกเรียกโดยไม่คำนึงถึงเส้นทางที่ goroutine ใช้? ขอบคุณ.
Alessandro Santini

2
หากคุณต้องการให้แน่ใจว่าฟังก์ชั่นไม่กลับมาก่อนที่กิจวัตรการเดินทางทั้งหมดจะเสร็จสิ้นใช่ว่าจะเลื่อนออกไป โดยปกติจุดทั้งหมดของกลุ่มการรอคือการรอจนกว่างานทั้งหมดจะเสร็จสิ้นเพื่อทำบางสิ่งกับผลลัพธ์ที่คุณรอคอย
Zanven

1
หากคุณไม่ใช้deferและหนึ่งใน goroutines ของคุณล้มเหลวในการโทรwg.Done()... คุณจะไม่Waitปิดกั้นตลอดไปหรือ? ดูเหมือนว่ามันอาจทำให้เกิดบั๊กที่หายากในโค้ดของคุณได้อย่างง่ายดาย ...
Dan Esparza

29

ฉันอยากจะแนะนำให้รวมการwg.Add()โทรลงในdoSomething()ฟังก์ชันนั้นเองดังนั้นหากคุณปรับจำนวนครั้งที่เรียกคุณไม่จำเป็นต้องปรับพารามิเตอร์เพิ่มแยกต่างหากด้วยตนเองซึ่งอาจทำให้เกิดข้อผิดพลาดหากคุณอัปเดต แต่ลืมอัปเดต อื่น ๆ (ในตัวอย่างเล็กน้อยที่ไม่น่าเป็นไปได้ แต่ถึงกระนั้นโดยส่วนตัวแล้วฉันเชื่อว่าเป็นการปฏิบัติที่ดีกว่าสำหรับการใช้โค้ดซ้ำ)

ดังที่ Stephen Weinberg ชี้ให้เห็นในคำตอบของเขาสำหรับคำถามนี้คุณจะต้องเพิ่มกลุ่มผู้รอก่อนที่จะวางไข่ gofunc แต่คุณสามารถทำได้อย่างง่ายดายโดยการห่อ 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
  • การปรับปรุงเล็กน้อยตามคำตอบของ Mroth
  • การใช้ defer สำหรับ 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")
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.