说我有一个功能
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
}()
}
答案 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例程运行的函数可以处理如何处理取消上下文。
答案 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
}