当使用通道来串行化任务时,异步流通常就诞生了。
异步编程很容易搞砸,尤其需求丰富,健壮度高时,比如拥有超时取消等。 一个经典的例子,是从文件逐行读取,交给另一个异步过程解析。通常I/O慢而计算快,为了最大化效率,一般会并行读取,放入缓存,再顺序处理,此时就需要管道来协调;如遇到读取错误,超时等,需终止整个流程。
我找到了一个通用模式,其内在逻辑很容易理解:
异步慢过程 后跟 同步快过程。
如果慢过程是 provider,则模式是:
1go provider()
2consumer()
一个完整的极简框架:
1// 异步生产端
2go func() {
3 defer close(ch)
4
5 for {
6 v, err := provide(ctx)
7 if err != nil {
8 return
9 }
10
11 select {
12 case ch <- v:
13 case <-ctx.Done():
14 return
15 }
16 }
17}()
18
19// 阻塞的消费端
20consume:
21for {
22 select {
23 // 收到结束信号时打断
24 case <-ctx.Done():
25 err = ctx.Err()
26 break consume
27 // 收到消费数据时
28 case v, ok := <-ch:
29 if !ok {
30 break consume
31 }
32
33 // 处理失败时打断
34 if err = process(v); err != nil {
35 break consume
36 }
37 }
38}
39
40// 消费终止后,主动发出结束信号,防止生产端 goroutine 泄露
41cancel()
42
43// 后续逻辑
44if err != nil {
45 log.Error(err)
46}
如果是 provider 的过程快而 consumer 过程慢呢?比如先通过计算,快速生成消息,再缓慢的写入到文件,则需要颠倒启动的顺序:
1go consumer()
2provider()
先启动的 consumer 始终在监听 provider 的通道获取消息,provider 则是一个阻塞的同步过程,生成消息。
祝我们在异步编程中好运!