如何与许多goroutine协调关闭

时间:2019-02-06 05:46:00

标签: go

说我有一个功能

type Foo struct {}

func (a *Foo) Bar() {
    // some expensive work - does some calls to redis
}

它会在我的应用程序中的某个goroutine中执行。其中许多可能在任何给定时间执行。在终止应用程序之前,我想确保所有剩余的goroutine已完成工作。

我可以做这样的事情吗?

type Foo struct {
    wg sync.WaitGroup
}

func (a *Foo) Close() {
    a.wg.Wait()
}

func (a *Foo) Bar() {
    a.wg.Add(1)
    defer a.wg.Done()

    // some expensive work - does some calls to redis
}

假设这里的Bar在goroutine中执行,并且其中许多可能在给定的时间运行,并且一旦调用Close并在sigterm或sigint上调用Close,就不应再调用Bar。

这有意义吗?

通常,我会看到Bar函数如下所示:

func (a *Foo) Bar() {
    a.wg.Add(1)

    go func() {
        defer a.wg.Done()
        // some expensive work - does some calls to redis
    }()
}

4 个答案:

答案 0 :(得分:0)

是的,WaitGroup是正确的答案。根据{{​​3}},您可以在计数器大于零的任何时间使用WaitGroup.Add

  

请注意,当计数器为零时发生的增量为正的调用必须在等待之前发生。在计数器大于零时开始的负增量呼叫或正增量呼叫可能随时发生。通常,这意味着对Add的调用应在创建goroutine或要等待的其他事件的语句之前执行。如果重新使用WaitGroup来等待几个独立的事件集,则必须在所有先前的Wait调用返回之后再进行新的Add调用。请参阅WaitGroup示例。

但是有一个技巧是,在调用Close之前,应始终使计数器大于零。这通常意味着您应该在wg.Add(或类似名称)中调用NewFoo,在wg.Done中调用Close。为了防止多次调用Done破坏等待组,应将Close包装到sync.Once中。您可能还希望阻止调用新的Bar()

答案 1 :(得分:0)

WaitGroup是一种方法,但是Go团队为您的用例完全引入了errgroup。 Leaf bebop回答中最不方便的部分是无视错误处理。错误处理是errgroup存在的原因。惯用的go代码应该从不吞下错误。

但是,保留您的Foo结构的签名(除修饰workerNumber之外)并且没有错误处理,我的建议如下所示:

package main

import (
    "fmt"
    "math/rand"
    "time"

    "golang.org/x/sync/errgroup"
)

type Foo struct {
    errg errgroup.Group
}

func NewFoo() *Foo {
    foo := &Foo{
        errg: errgroup.Group{},
    }
    return foo
}

func (a *Foo) Bar(workerNumber int) {
    a.errg.Go(func() error {
        select {
        // simulates the long running clals
        case <-time.After(time.Second * time.Duration(rand.Intn(10))):
            fmt.Println(fmt.Sprintf("worker %d completed its work", workerNumber))
            return nil
        }
    })
}

func (a *Foo) Close() {
    a.errg.Wait()
}

func main() {
    foo := NewFoo()

    for i := 0; i < 10; i++ {
        foo.Bar(i)
    }

    <-time.After(time.Second * 5)
    fmt.Println("Waiting for workers to complete...")
    foo.Close()
    fmt.Println("Done.")
}

这样做的好处是,如果您在代码中引入了错误处理(应该这样做),则只需稍微修改一下此代码:简而言之,errg.Wait()将返回第一个redis错误,而{{1 }}可以在整个堆栈中传播(在这种情况下,传播到主要对象)。

同样,利用Close()软件包,如果失败,您还可以立即取消任何正在运行的redis调用。 context.Context文档中有一些示例。

答案 2 :(得分:0)

我认为无限期地等待所有go例程完成不是正确的方法。 如果其中一个go例程被阻塞或说由于某种原因而挂起,但从未成功终止,那么该怎么办?杀死该进程或等待go例程完成?

相反,无论所有例程是否已完成,您都应等待一段时间并终止应用程序。

编辑:原始ans 感谢@leaf bebop指出来。我误解了这个问题。

上下文包可用于向所有go例程发出信号以处理终止信号。

appCtx, cancel := context.WithCancel(context.Background())

必须将appCtx传递给所有go例程。

在退出信号呼叫cancel()上。

作为go例程运行的函数可以处理如何处理取消上下文。

Using context cancellation in Go

答案 3 :(得分:0)

我经常使用的模式是:https://play.golang.org/p/ibMz36TS62z

package main

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

type response struct {
    message string
}

func task(i int, done chan response) {
    time.Sleep(1 * time.Second)
    done <- response{fmt.Sprintf("%d done", i)}
}

func main() {

    responses := GetResponses(10)

    fmt.Println("all done", len(responses))
}

func GetResponses(n int) []response {
    donequeue := make(chan response)
    wg := sync.WaitGroup{}
    for i := 0; i < n; i++ {
        wg.Add(1)
        go func(value int) {
            defer wg.Done()
            task(value, donequeue)
        }(i)
    }
    go func() {
        wg.Wait()
        close(donequeue)
    }()
    responses := []response{}
    for result := range donequeue {
        responses = append(responses, result)
    }

    return responses
}

这也使节流变得容易:https://play.golang.org/p/a4MKwJKj634

package main

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

type response struct {
    message string
}

func task(i int, done chan response) {
    time.Sleep(1 * time.Second)
    done <- response{fmt.Sprintf("%d done", i)}
}

func main() {

    responses := GetResponses(10, 2)

    fmt.Println("all done", len(responses))
}

func GetResponses(n, concurrent int) []response {

    throttle := make(chan int, concurrent)
    for i := 0; i < concurrent; i++ {
        throttle <- i
    }
    donequeue := make(chan response)
    wg := sync.WaitGroup{}
    for i := 0; i < n; i++ {
        wg.Add(1)
        <-throttle
        go func(value int) {
            defer wg.Done()
            throttle <- 1
            task(value, donequeue)
        }(i)
    }
    go func() {
        wg.Wait()
        close(donequeue)
    }()
    responses := []response{}
    for result := range donequeue {
        responses = append(responses, result)
    }

    return responses
}