Go中的复杂管道(第四部分)-理智的协调和取消

74 阅读3分钟

理智的取消和协调

在前两篇文章中,我们介绍了实现负责持久化解析数据的过程的步骤。独立地,这两个过程的工作是完美的,但是当需要通信时,事情就开始变得复杂了,特别是在传播错误和协调通信方面。

幸好 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() 的情况是真正驱动这个包背后的所有逻辑。

下一步是什么?

下一篇博文将是这个系列的结论,我们将把所有东西组合成最终的工具,解决我们在第一部分定义的问题。我们就快到了。