Làm thế nào để đợi tất cả các goroutines kết thúc mà không cần sử dụng thời gian.


108

Mã này chọn tất cả các tệp xml trong cùng một thư mục, vì tệp thực thi được gọi và áp dụng xử lý không đồng bộ cho từng kết quả trong phương thức gọi lại (trong ví dụ bên dưới, chỉ tên của tệp được in ra).

Làm cách nào để tránh sử dụng phương pháp ngủ để ngăn phương thức chính thoát ra? Tôi gặp sự cố khi xoay quanh các kênh (tôi cho rằng đó là những gì cần thiết để đồng bộ hóa kết quả) vì vậy mọi trợ giúp đều được đánh giá cao!

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

Câu trả lời:


173

Bạn có thể sử dụng sync.WaitGroup . Trích dẫn ví dụ được liên kết:

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
Bất kỳ lý do gì bạn phải làm wg.Add (1) bên ngoài thói quen di chuyển? Chúng ta có thể làm điều đó bên trong ngay trước khi defer wg.Done () không?
sat

18
sat, vâng, có một lý do, nó được mô tả trong tài liệu sync.WaitGroup.Add: 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
Việc điều chỉnh mã này gây ra cho tôi một phiên gỡ lỗi dài vì goroutine của tôi là một hàm được đặt tên và việc chuyển vào WaitGroup dưới dạng giá trị sẽ sao chép nó và làm cho wg.Done () không hiệu quả. Mặc dù điều này có thể được khắc phục bằng cách chuyển một con trỏ & wg, nhưng một cách tốt hơn để ngăn chặn các lỗi như vậy là khai báo biến WaitGroup ở vị trí đầu tiên: wg := new(sync.WaitGroup)thay vì var wg sync.WaitGroup.
Robert Jack sẽ

Tôi đoán viết wg.Add(len(urls))ngay phía trên dòng là hợp lệ for _, url := range urls, tôi tin rằng sẽ tốt hơn khi bạn chỉ sử dụng Add một lần.
Victor

@RobertJackWill: Lưu ý tốt! BTW, điều này được đề cập trong tài liệu : "Không được sao chép WaitGroup sau lần sử dụng đầu tiên. Quá tệ là Go không có cách thực thi điều này . Tuy nhiên, trên thực tế, go vetnó phát hiện trường hợp này và cảnh báo bằng" func pass lock by value : sync.WaitGroup chứa sync.noCopy ".
Brent Bradburn

56

WaitGroups chắc chắn là cách chuẩn để làm điều này. Tuy nhiên, chỉ vì mục đích hoàn chỉnh, đây là giải pháp thường được sử dụng trước khi WaitGroups được giới thiệu. Ý tưởng cơ bản là sử dụng một kênh để nói "Tôi đã hoàn thành" và yêu cầu quy trình chính đợi cho đến khi mỗi quy trình sinh sản báo cáo đã hoàn thành.

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
Rất vui khi thấy một giải pháp với các kênh đơn giản. Một tiền thưởng thêm: nếu doSomething()kết quả trả lại một số người, hơn bạn có thể đặt trên các kênh, và bạn có thể thu thập và xử lý các kết quả trong lần thứ hai vòng lặp for (ngay sau khi họ đã sẵn sàng)
Andras

4
Nó chỉ hoạt động nếu bạn đã biết số lượng quy trình bạn muốn bắt đầu. Điều gì sẽ xảy ra nếu bạn đang viết một số loại trình thu thập thông tin html và bắt đầu gorutines theo cách đệ quy cho mọi liên kết trên trang?
glossdev

Bạn sẽ cần phải theo dõi điều này bằng cách nào đó bất kể. Với WaitGroups thì dễ dàng hơn một chút vì mỗi lần bạn sinh ra một quy trình mới, trước tiên bạn có thể thực hiện wg.Add(1)và do đó nó sẽ theo dõi chúng. Với các kênh sẽ khó hơn một chút.
joshlf

c sẽ chặn vì tất cả các quy trình hoạt động sẽ cố gắng truy cập vào nó và nó không được đệm
Edwin Ikechukwu Okonkwo 20/02

Nếu bằng "block", bạn có nghĩa là chương trình sẽ deadlock, điều đó không đúng. Bạn có thể thử tự chạy nó. Lý do là bởi các goroutines duy nhất được ghi vào ckhác với goroutine chính, đọc từ đó c. Do đó, quy trình chính luôn có sẵn để đọc một giá trị ngoài kênh, điều này sẽ xảy ra khi một trong các quy trình có sẵn để ghi một giá trị vào kênh. Bạn nói đúng rằng nếu mã này không tạo ra các goroutines mà thay vào đó chạy mọi thứ trong một goroutine duy nhất, nó sẽ bế tắc.
joshlf

8

sync.WaitGroup có thể giúp bạn ở đây.

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

Mặc dù sync.waitGroup(wg) là phương thức chuẩn nhưng nó yêu cầu bạn thực hiện ít nhất một số wg.Addcuộc gọi của mình trước khi wg.Waittất cả hoàn thành. Điều này có thể không khả thi đối với những thứ đơn giản như trình thu thập thông tin web, nơi bạn không biết trước số lượng cuộc gọi đệ quy và phải mất một lúc để truy xuất dữ liệu thúc đẩy wg.Addcuộc gọi. Sau cùng, bạn cần tải và phân tích cú pháp trang đầu tiên trước khi biết kích thước của loạt trang con đầu tiên.

Tôi đã viết một giải pháp bằng cách sử dụng các kênh, tránh waitGrouptrong giải pháp của tôi là Bài tập về chuyến đi của trình thu thập thông tin web . Mỗi khi một hoặc nhiều quy trình bắt đầu, bạn sẽ gửi số tới childrenkênh. Mỗi khi một quy trình di chuyển sắp hoàn thành, bạn gửi 1đến donekênh. Khi tổng số con bằng tổng số đã làm, chúng ta hoàn thành.

Mối quan tâm duy nhất còn lại của tôi là kích thước được mã hóa cứng của resultskênh, nhưng đó là hạn chế của Go (hiện tại).


// 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
            }
        }
    }
}

Mã nguồn đầy đủ cho giải pháp


1

Đây là một giải pháp sử dụng WaitGroup.

Đầu tiên, xác định 2 phương thức tiện ích:

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

Sau đó, thay thế lời gọi của callback:

go callback(fileName)

Với một cuộc gọi đến chức năng tiện ích của bạn:

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

Bước cuối cùng, thêm dòng này vào cuối của bạn main, thay vì của bạn sleep. Điều này sẽ đảm bảo luồng chính đang đợi tất cả các quy trình kết thúc trước khi chương trình có thể dừng lại.

func main() {
  // ...
  util.WaitForAllNodes()
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.