考虑这个功能:
func doAllWork() error {
var wg sync.WaitGroup
wg.Add(3)
for i := 0; i < 2; i++ {
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
result, err := work(j)
if err != nil {
// can't use `return err` here
// what sould I put instead ?
os.Exit(0)
}
}
}()
}
wg.Wait()
return nil
}
在每个goroutine中,函数work()
被调用10次。如果对work()
的一次调用在任何正在运行的goroutine中返回错误,我希望所有goroutine立即停止,并退出程序。
可以在这里使用os.Exit()
吗?我该怎么处理?
修改:此问题与how to stop a goroutine不同,因为如果在一个
中发生错误,我需要关闭所有goroutine答案 0 :(得分:20)
您可以使用为此类内容创建的context
包(&#34;带有截止日期,取消信号......&#34; )。
您创建了一个能够使用context.WithCancel()
发布取消信号的上下文(父上下文可能是context.Background()
返回的上下文)。这将返回一个cancel()
函数,该函数可用于取消(或更准确地说信号取消意图)给工作人员g。
并且在工作器goroutines中,您必须通过检查Context.Done()
返回的通道是否已关闭来检查是否已启动此类意图,最简单的方法是尝试从中接收(如果它已关闭则立即进行)。要进行非阻塞检查(如果未关闭则可以继续),请将select
语句与default
分支一起使用。
我将使用以下work()
实现,它模拟10%的失败几率,并模拟1秒的工作:
func work(i int) (int, error) {
if rand.Intn(100) < 10 { // 10% of failure
return 0, errors.New("random error")
}
time.Sleep(time.Second)
return 100 + i, nil
}
doAllWork()
可能如下所示:
func doAllWork() error {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Make sure it's called to release resources even if no errors
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := 0; j < 10; j++ {
// Check if any error occurred in any other gorouties:
select {
case <-ctx.Done():
return // Error somewhere, terminate
default: // Default is must to avoid blocking
}
result, err := work(j)
if err != nil {
fmt.Printf("Worker #%d during %d, error: %v\n", i, j, err)
cancel()
return
}
fmt.Printf("Worker #%d finished %d, result: %d.\n", i, j, result)
}
}(i)
}
wg.Wait()
return ctx.Err()
}
这是如何测试的:
func main() {
rand.Seed(time.Now().UnixNano() + 1) // +1 'cause Playground's time is fixed
fmt.Printf("doAllWork: %v\n", doAllWork())
}
输出(在Go Playground上尝试):
Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #1 during 4, error: random error
Worker #0 finished 3, result: 103.
doAllWork: context canceled
如果没有错误,例如使用以下work()
函数时:
func work(i int) (int, error) {
time.Sleep(time.Second)
return 100 + i, nil
}
输出就像(在Go Playground上试试):
Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #0 finished 3, result: 103.
Worker #0 finished 4, result: 104.
Worker #1 finished 4, result: 104.
Worker #1 finished 5, result: 105.
Worker #0 finished 5, result: 105.
Worker #0 finished 6, result: 106.
Worker #1 finished 6, result: 106.
Worker #1 finished 7, result: 107.
Worker #0 finished 7, result: 107.
Worker #0 finished 8, result: 108.
Worker #1 finished 8, result: 108.
Worker #1 finished 9, result: 109.
Worker #0 finished 9, result: 109.
doAllWork: <nil>
备注:强>
基本上我们只使用了上下文的Done()
频道,所以看起来我们可以轻松(如果不是更容易)使用done
频道而不是Context
,关闭在上述解决方案中执行cancel()
所做的工作的渠道。
事实并非如此。 这只能在只有一个goroutine关闭频道的情况下使用,但在我们的情况下,任何工作人员都可以这样做。并尝试关闭已经关闭的频道恐慌(详见此处:{{3 }})。因此,您必须确保在close(done)
周围进行某种同步/排除,这会降低其可读性,甚至更复杂。实际上,这正是cancel()
函数在引擎盖下所做的事情,隐藏/抽象远离你的眼睛,因此可以多次调用cancel()
以使你的代码/使用它更简单。
为此,您可以使用错误频道:
errs := make(chan error, 2) // Buffer for 2 errors
当遇到错误时,在工作人员内部,将其发送到通道而不是打印它:
result, err := work(j)
if err != nil {
errs <- fmt.Errorf("Worker #%d during %d, error: %v\n", i, j, err)
cancel()
return
}
在循环之后,如果出现错误,请返回(否则nil
):
// Return (first) error, if any:
if ctx.Err() != nil {
return <-errs
}
return nil
此次输出(在How does a non initialized channel behave?上尝试此操作):
Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #0 finished 3, result: 103.
doAllWork: Worker #1 during 4, error: random error
请注意,我使用的缓冲通道的缓冲区大小等于worker的数量,这可确保在其上发送始终是非阻塞的。这也使您可以接收和处理所有错误,而不仅仅是一个(例如第一个)。另一个选择可能是使用缓冲通道仅保持1,并对其执行非阻塞发送,这可能如下所示:
errs := make(chan error, 1) // Buffered only for the first error
// ...and inside the worker:
result, err := work(j)
if err != nil {
// Non-blocking send:
select {
case errs <- fmt.Errorf("Worker #%d during %d, error: %v\n", i, j, err):
default:
}
cancel()
return
}
答案 1 :(得分:4)
一种更清晰的选择是使用errgroup
(documentation)。
程序包errgroup
为处理共同任务的子任务的goroutine组提供同步,错误传播和上下文取消。
您可以在此示例(playground)中进行检查:
var g errgroup.Group
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.somestupidname.com/",
}
for _, url := range urls {
// Launch a goroutine to fetch the URL.
url := url // https://golang.org/doc/faq#closures_and_goroutines
g.Go(func() error {
// Fetch the URL.
resp, err := http.Get(url)
if err == nil {
resp.Body.Close()
}
return err
})
}
// Wait for all HTTP fetches to complete.
if err := g.Wait(); err == nil {
fmt.Println("Successfully fetched all URLs.")
} else {
// After all have run, at least one of them has returned an error!
// But all have to finish their work!
// If you want to stop others goroutines when one fail, go ahead reading!
fmt.Println("Unsuccessfully fetched URLs.")
}
但是要注意:Go
documentation中的The first call to return a non-nil error cancels the group
短语有点误导。
实际上,errgroup.Group
如果是使用上下文创建的(WithContext
函数),则在执行goroutine时将调用WithContext
返回的上下文的cancel函数该组中的错误将返回错误,否则将不执行任何操作(read the source code here!)。
因此,如果要关闭不同的goroutine,则必须使用返回的WithContext
上下文并由您自己在其中进行管理,errgroup
只会关闭该上下文!
Here you can find an example.
总而言之,errgroup
可以以不同的方式使用,如examples所示。
“只是错误”,如上例所示:
Wait
等待所有goroutine结束,然后返回第一个非空错误(如果有),或者返回nil
。
并行:
您必须使用WithContext
function创建组,并使用上下文来管理上下文关闭。
I created a playground example here with some sleeps!
您必须手动关闭每个goroutine,但是使用上下文可以在关闭上下文时结束它们。
管道(请参见examples中的更多信息)。
答案 2 :(得分:0)
这里的另一种方法是使用errgroup.WithContext
。您可以在此example中进行检查。
简而言之,g.Wait()
等待第一个错误发生或等待所有错误完成。如果任何goroutine中发生错误(在提供的示例中超时),它将通过ctx.Done()
通道取消其他goroutine中的执行。