理智的取消和协调
在前两篇文章中,我们介绍了实现负责持久化和解析数据的过程的步骤。独立地,这两个过程的工作是完美的,但是当需要通信时,事情就开始变得复杂了,特别是在传播错误和协调通信方面。
幸好 errgroup涵盖了我们为实现这一目标可能需要的一切,它为在一个共同任务的子任务上工作的goroutines组提供了同步、错误传播和Context取消。
回想一下,我们解决这个问题的目标是使用并行的goroutines,虽然在技术上可以只使用标准库来解决同样的问题(例如通过一堆chan error ),但不使用errgroup ,会使代码更难实现和维护。只需考虑处理错误传播或多个goroutines之间取消的不同方法,这虽然是一个问题,但很快就会失控。
最低要求
所有与本帖相关的代码都在Github上,请随意浏览以了解更多细节,以下是运行本例的最低要求。
- Go 1.14
- 第二部分所需的软件包。
了解 errgroup
包errgroup ,有一个函数叫errgroup.WithContext ,这个函数返回一个errgroup.Group 的实例,这个类型有两个方法。
Group.Go接收一个函数,通过一个goroutine调用,并期望返回一个错误;和Group.Wait阻止所有的goroutine完成(所有的goroutine都不返回错误),或者任何一个goroutine返回错误。
这个API很简单,但却很强大。在实践中,他们要始终牢记的关键重要的事情是确保由Group.Go 接收的函数正确处理context.Done() ,通过select ,这就是正确处理多个运行中的goroutine的取消。
例如,考虑这个完整的例子。
package main
import (
"context"
"fmt"
"log"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
ctx, cancel := context.WithDeadline(context.Background(),
time.Now().Add(200*time.Millisecond)) // Replace "200" with anything larger than 400
defer cancel()
messages := make(chan string)
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
defer close(messages)
for _, msg := range []string{"a", "b", "c", "d"} {
select {
case messages <- msg:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
g.Go(func() error {
for {
select {
case msg, open := <-messages:
if !open {
return nil
}
fmt.Println(msg)
case <-ctx.Done():
return ctx.Err()
}
time.Sleep(100 * time.Millisecond)
}
return nil
})
if err := g.Wait(); err != nil {
log.Fatalln("wait", err)
}
}
运行上面的例子总是会失败,这是因为有3个原因。
- 我们定义了一个200毫秒的期限。
- 处理每个消息需要100毫秒。
- 我们试图处理4条消息,因此我们需要至少~400毫秒来完成整个过程。
如果我们把200毫秒的值换成大于400毫秒的值,我们应该总能看到这个例子的工作。
现在,让我们稍微改变一下,不要定义一个截止日期,如果其中一个goroutine失败了怎么办?比如说负责接收消息的那个。
package main
import (
"context"
"errors"
"log"
"golang.org/x/sync/errgroup"
)
func main() {
messages := make(chan string)
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
defer close(messages)
for _, msg := range []string{"a", "b", "c", "d"} {
select {
case messages <- msg:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
g.Go(func() error {
for {
select {
case msg, open := <-messages:
if !open {
return nil
}
if msg == "c" {
return errors.New("I don't like c")
}
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
if err := g.Wait(); err != nil {
log.Fatalln("wait", err)
}
}
错误仍然会正确地传播给另一个goroutine。这两个例子绝对是简单的,但是当你开始给管道添加步骤时,你会立即发现事情会很快变得复杂。
我不想听起来像个破唱片,但很明显,使用select 和处理<-ctx.Done() 的情况是真正驱动这个包背后的所有逻辑。
下一步是什么?
下一篇博文将是这个系列的结论,我们将把所有东西组合成最终的工具,解决我们在第一部分定义的问题。我们就快到了。