如何在不使用time.Sleep的情况下等待所有goroutine完成?


108

这段代码将选择同一文件夹中的所有xml文件,作为被调用的可执行文件,并以异步方式将处​​理应用于回调方法中的每个结果(在下面的示例中,仅打印出文件名)。

如何避免使用sleep方法阻止main方法退出?我在解决问题时遇到了麻烦(我想这就是同步结果所需要的),因此对您有所帮助!

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

Answers:


173

您可以使用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
您是否必须在go例程之外执行wg.Add(1)?我们可以在延迟wg.Done()之前在内部执行此操作吗?
2014年

18
坐,是的,有一个原因,它在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 2014年

15
修改此代码使我花费了很长的调试时间,因为我的goroutine是一个命名函数,将WaitGroup作为值传递会复制它并使wg.Done()无效。虽然可以通过传递指针&wg来解决此问题,但防止此类错误的更好方法是首先将WaitGroup变量声明为指针:wg := new(sync.WaitGroup)而不是var wg sync.WaitGroup
罗伯特·杰克·威尔

我想wg.Add(len(urls))只在行上方写是有效的for _, url := range urls,我相信最好,因为您只使用一次添加。
维克多

@RobertJackWill:请注意!顺便说一句,这涵盖在文档中:“ WaitGroup不能在第一次使用后复制。太糟糕了,Go没有强制执行此方法的方法。但是,实际上,go vet确实会检测到这种情况,并发出警告,“ func按值传递锁:sync.WaitGroup包含sync.noCopy。”
布伦特布拉德

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()返回某些结果,则可以将其放置在频道上,并且可以在第二个for循环中收集和处理结果(一旦它们准备好)
andras

4
仅当您已经知道要启动的gorutine数量时,它才有效。如果您正在编写某种html搜寻器并为页面上的每个链接以递归方式启动gorutines,该怎么办?
Shinydev '16

无论如何,您都需要对此进行跟踪。使用WaitGroups会容易一些,因为每次您生成新的goroutine时,您都可以先进行操作wg.Add(1),从而跟踪它们。使用渠道会更加困难。
joshlf

c会阻塞,因为所有go例程都会尝试访问它,并且它没有缓冲
Edwin Ikechukwu Okonkwo

如果按“块”表示您将使程序死锁,那不是真的。您可以尝试自己运行它。原因是唯一要写入c的goroutine与主要的goroutine(从读取)不同c。因此,主goroutine始终可用于从通道读取值,当goroutine之一可用于将值写入通道时,将发生这种情况。没错,如果此代码未生成goroutine,而是在单个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完成。对于诸如Web爬网程序之类的简单事物,这可能不可行,在这种情况下,您事先不知道递归调用的数量,并且需要花费一些时间来检索驱动wg.Add调用的数据。毕竟,您需要加载并解析第一页,然后才能知道第一批子页的大小。

我使用渠道编写了一个解决方案,waitGroup在解决方案中避免了“ Tour of Go-网络爬虫”练习。每次启动一个或多个go-routines时,您就将数字发送到children通道。每当go例程即将完成时,您都会将a发送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()
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.