จะรอให้ goroutines ทั้งหมดเสร็จได้อย่างไรโดยไม่ต้องใช้เวลานอนหลับ?


109

รหัสนี้เลือกไฟล์ xml ทั้งหมดในโฟลเดอร์เดียวกันเนื่องจากไฟล์ปฏิบัติการที่เรียกใช้และอะซิงโครนัสจะใช้การประมวลผลกับแต่ละผลลัพธ์ในวิธีการโทรกลับ (ในตัวอย่างด้านล่างจะพิมพ์เฉพาะชื่อของไฟล์เท่านั้น)

ฉันจะหลีกเลี่ยงการใช้วิธีการนอนหลับเพื่อป้องกันไม่ให้วิธีหลักออกไปได้อย่างไร? ฉันมีปัญหาในการห่อหัวของฉันไปรอบ ๆ ช่อง (ฉันคิดว่านั่นคือสิ่งที่ต้องใช้ในการซิงโครไนซ์ผลลัพธ์) ดังนั้นขอขอบคุณความช่วยเหลือใด ๆ !

package main

import (
    "fmt"
    "io/ioutil"
    "path"
    "path/filepath"
    "os"
    "runtime"
    "time"
)

func eachFile(extension string, callback func(file string)) {
    exeDir := filepath.Dir(os.Args[0])
    files, _ := ioutil.ReadDir(exeDir)
    for _, f := range files {
            fileName := f.Name()
            if extension == path.Ext(fileName) {
                go callback(fileName)
            }
    }
}


func main() {
    maxProcs := runtime.NumCPU()
    runtime.GOMAXPROCS(maxProcs)

    eachFile(".xml", func(fileName string) {
                // Custom logic goes in here
                fmt.Println(fileName)
            })

    // This is what i want to get rid of
    time.Sleep(100 * time.Millisecond)
}

คำตอบ:


174

คุณสามารถใช้sync.WaitGroup อ้างถึงตัวอย่างที่เชื่อมโยง:

package main

import (
        "net/http"
        "sync"
)

func main() {
        var wg sync.WaitGroup
        var urls = []string{
                "http://www.golang.org/",
                "http://www.google.com/",
                "http://www.somestupidname.com/",
        }
        for _, url := range urls {
                // Increment the WaitGroup counter.
                wg.Add(1)
                // Launch a goroutine to fetch the URL.
                go func(url string) {
                        // Decrement the counter when the goroutine completes.
                        defer wg.Done()
                        // Fetch the URL.
                        http.Get(url)
                }(url)
        }
        // Wait for all HTTP fetches to complete.
        wg.Wait()
}

11
เหตุผลใดที่คุณต้องทำ wg เพิ่ม (1) นอกกิจวัตรการเดินทาง? เราสามารถทำได้ภายในก่อนการเลื่อน wg.Done ()?
ส.

18
นั่งใช่มีเหตุผลอธิบายไว้ในการซิงค์รอกลุ่มเพิ่มเอกสาร: Note that calls with positive delta must happen before the call to Wait, or else Wait may wait for too small a group. Typically this means the calls to Add should execute before the statement creating the goroutine or other event to be waited for. See the WaitGroup example.
wobmene

15
การปรับรหัสนี้ทำให้ฉันมีเซสชันการดีบักเป็นเวลานานเนื่องจาก goroutine ของฉันเป็นฟังก์ชันที่มีชื่อและการส่งผ่าน WaitGroup เป็นค่าจะคัดลอกและทำให้ wg.Done () ไม่ได้ผล ขณะนี้อาจได้รับการแก้ไขโดยผ่านตัวชี้ & wg เป็นวิธีที่ดีกว่าเพื่อป้องกันข้อผิดพลาดดังกล่าวคือการประกาศตัวแปร WaitGroup เป็นชี้ในสถานที่แรก: แทนwg := new(sync.WaitGroup) var wg sync.WaitGroup
Robert Jack Will

ฉันเดาว่ามันถูกต้องที่จะเขียนwg.Add(len(urls))เหนือบรรทัดfor _, url := range urlsฉันเชื่อว่าจะดีกว่าเมื่อคุณใช้ Add เพียงครั้งเดียว
วิกเตอร์

@RobertJackWill: หมายเหตุดี! BTW สิ่งนี้ครอบคลุมอยู่ในเอกสาร : "ต้องไม่คัดลอก WaitGroup หลังจากใช้งานครั้งแรกน่าเสียดายที่ Go ไม่มีวิธีบังคับใช้อย่างไรก็ตามที่จริงgo vetตรวจพบกรณีนี้และเตือนด้วย" func pass lock by value : sync.WaitGroup มี sync.noCopy ".
Brent Bradburn

56

WaitGroups เป็นวิธีที่ยอมรับได้ในการทำเช่นนี้ เพื่อความสมบูรณ์แม้ว่านี่คือโซลูชันที่ใช้กันทั่วไปก่อนที่ WaitGroups จะได้รับการแนะนำ แนวคิดพื้นฐานคือการใช้ช่องเพื่อพูดว่า "เสร็จแล้ว" และให้ goroutine หลักรอจนกว่ากิจวัตรที่สร้างแต่ละครั้งจะรายงานเสร็จสิ้น

func main() {
    c := make(chan struct{}) // We don't need any data to be passed, so use an empty struct
    for i := 0; i < 100; i++ {
        go func() {
            doSomething()
            c <- struct{}{} // signal that the routine has completed
        }()
    }

    // Since we spawned 100 routines, receive 100 messages.
    for i := 0; i < 100; i++ {
        <- c
    }
}

9
ยินดีที่ได้เห็นวิธีแก้ปัญหาด้วยช่องธรรมดา โบนัสเพิ่มเติม: หากdoSomething()ส่งคืนผลลัพธ์บางอย่างมากกว่าที่คุณสามารถวางไว้ในช่องและคุณสามารถรวบรวมและประมวลผลผลลัพธ์ในวินาทีสำหรับการวนซ้ำ (ทันทีที่พร้อม)
andras

4
ใช้งานได้ก็ต่อเมื่อคุณทราบจำนวนโกรทีนที่คุณต้องการเริ่มต้นแล้ว จะเกิดอะไรขึ้นถ้าคุณกำลังเขียนโปรแกรมรวบรวมข้อมูล html และเริ่ม gorutines ในลักษณะวนซ้ำสำหรับทุกลิงก์ในหน้า
shinydev

คุณจะต้องติดตามสิ่งนี้โดยไม่คำนึงถึง ด้วย WaitGroups มันง่ายกว่าเล็กน้อยเพราะทุกครั้งที่คุณวางไข่ goroutine ใหม่คุณสามารถทำได้ก่อนwg.Add(1)และมันจะติดตามพวกเขา ด้วยช่องมันจะค่อนข้างยาก
joshlf

c จะบล็อกเนื่องจากกิจวัตรการไปทั้งหมดจะพยายามเข้าถึงและมันไม่มีบัฟเฟอร์
Edwin Ikechukwu Okonkwo

ถ้าโดย "บล็อก" คุณหมายความว่าโปรแกรมจะหยุดชะงักนั่นไม่เป็นความจริง คุณสามารถลองรันด้วยตัวคุณเอง เหตุผลก็คือว่า goroutines เพียงการเขียนที่cแตกต่างจาก goroutine cหลักซึ่งอ่านจาก ดังนั้น goroutine หลักจึงพร้อมใช้งานเพื่ออ่านค่านอกช่องสัญญาณเสมอซึ่งจะเกิดขึ้นเมื่อมี goroutines ตัวใดตัวหนึ่งเพื่อเขียนค่าลงในช่อง คุณคิดถูกแล้วที่ถ้ารหัสนี้ไม่ได้สร้าง goroutines แต่ใช้ทุกอย่างใน goroutine ตัวเดียวแทนมันจะชะงักงัน
joshlf

8

sync.WaitGroupสามารถช่วยคุณได้ที่นี่

package main

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


func wait(seconds int, wg * sync.WaitGroup) {
    defer wg.Done()

    time.Sleep(time.Duration(seconds) * time.Second)
    fmt.Println("Slept ", seconds, " seconds ..")
}


func main() {
    var wg sync.WaitGroup

    for i := 0; i <= 5; i++ {
        wg.Add(1)   
        go wait(i, &wg)
    }
    wg.Wait()
}

1

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

ฉันเขียนวิธีแก้ปัญหาโดยใช้ช่องทางโดยหลีกเลี่ยงwaitGroupการใช้Tour of Go -แบบฝึกหัดโปรแกรมรวบรวมข้อมูลเว็บในโซลูชันของฉัน ทุกครั้งที่เริ่มกิจวัตรประจำวันอย่างน้อยหนึ่งรายการคุณจะส่งหมายเลขไปที่childrenช่อง ทุกครั้งที่กิจวัตรการเดินทางกำลังจะเสร็จสิ้นคุณจะส่ง1ไปที่doneช่อง เมื่อผลรวมของเด็กเท่ากับผลรวมของการทำเราก็ทำเสร็จแล้ว

ข้อกังวลเดียวที่เหลืออยู่ของฉันคือขนาดของresultsช่องแบบฮาร์ดโค้ดแต่นั่นเป็นข้อ จำกัด (ปัจจุบัน) Go


// recursionController is a data structure with three channels to control our Crawl recursion.
// Tried to use sync.waitGroup in a previous version, but I was unhappy with the mandatory sleep.
// The idea is to have three channels, counting the outstanding calls (children), completed calls 
// (done) and results (results).  Once outstanding calls == completed calls we are done (if you are
// sufficiently careful to signal any new children before closing your current one, as you may be the last one).
//
type recursionController struct {
    results  chan string
    children chan int
    done     chan int
}

// instead of instantiating one instance, as we did above, use a more idiomatic Go solution
func NewRecursionController() recursionController {
    // we buffer results to 1000, so we cannot crawl more pages than that.  
    return recursionController{make(chan string, 1000), make(chan int), make(chan int)}
}

// recursionController.Add: convenience function to add children to controller (similar to waitGroup)
func (rc recursionController) Add(children int) {
    rc.children <- children
}

// recursionController.Done: convenience function to remove a child from controller (similar to waitGroup)
func (rc recursionController) Done() {
    rc.done <- 1
}

// recursionController.Wait will wait until all children are done
func (rc recursionController) Wait() {
    fmt.Println("Controller waiting...")
    var children, done int
    for {
        select {
        case childrenDelta := <-rc.children:
            children += childrenDelta
            // fmt.Printf("children found %v total %v\n", childrenDelta, children)
        case <-rc.done:
            done += 1
            // fmt.Println("done found", done)
        default:
            if done > 0 && children == done {
                fmt.Printf("Controller exiting, done = %v, children =  %v\n", done, children)
                close(rc.results)
                return
            }
        }
    }
}

ซอร์สโค้ดแบบเต็มสำหรับโซลูชัน


1

นี่คือโซลูชันที่ใช้ WaitGroup

ขั้นแรกกำหนด 2 วิธียูทิลิตี้:

package util

import (
    "sync"
)

var allNodesWaitGroup sync.WaitGroup

func GoNode(f func()) {
    allNodesWaitGroup.Add(1)
    go func() {
        defer allNodesWaitGroup.Done()
        f()
    }()
}

func WaitForAllNodes() {
    allNodesWaitGroup.Wait()
}

จากนั้นแทนที่การเรียกร้องของcallback:

go callback(fileName)

ด้วยการเรียกใช้ฟังก์ชันยูทิลิตี้ของคุณ:

util.GoNode(func() { callback(fileName) })

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

func main() {
  // ...
  util.WaitForAllNodes()
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.