文章目录
- 1.全部返回
- 2.出错及时返回
- 3.最早成功返回
- 4.小结
- 参考文献
Go 为并发而生。在使用 Go 编写并发程序时,我们应该熟悉常见的并发模式。虽然业务开发中常用的可能只有那么一两种,但还是有必要了解一下,因为面试可能会被问到。
Go 并发模式指的是对并发协程的管理方式,根据不同的业务场景要求,大概可分为如下几种。
1.全部返回
全部返回指的是调用下游接口不管失败还是成功,需要等待所有的接口执行完毕。
这种应该是最常见的并发模式,一般使用 Go 官方提供的包 errgroup 便可轻松完成。
假设有三个下游接口需要被调用,这里用三个函数来模拟,并给出不同的耗时。
代码语言:javascript复制func api1() (int, error) {
time.Sleep(time.Second)
return 1, nil
}
func api2() (int, error) {
time.Sleep(2*time.Second)
return 2, nil
}
func api3() (int, error) {
time.Sleep(3*time.Second)
return 3, nil
}
使用 errgroup 完成并发调用,并等待所有接口返回。
代码语言:javascript复制package main
import (
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
var eg errgroup.Group
var ret1, ret2, ret3 int
now := time.Now()
eg.Go(func() error {
var err error
ret1, err = api1()
return err
})
eg.Go(func() error {
var err error
ret2, err = api2()
return err
})
eg.Go(func() error {
var err error
ret3, err = api3()
return err
})
err := eg.Wait()
cost := time.Since(now)
fmt.Printf("err:%v cost:%v ret1:%v ret2:%v ret3:%vn", err, cost, ret1, ret2, ret3)
}
运行输出:
代码语言:javascript复制err:<nil> cost:3.0012229ss ret1:1 ret2:2 ret3:3
通过耗时 cost 为 3s 可见,并发调用下游接口的耗时大约等于其中耗时最久的接口 api3 的耗时。
2.出错及时返回
如果所有的接口都需要成功,业务逻辑上才算成功。那么,当有一个接口返回失败时,其他接口无需再继续等待,即出现错误需及时返回。
还是以三个函数模拟下游被调的接口,假设其中接口 api1 调用发生了失败。
代码语言:javascript复制func api1() (int, error) {
time.Sleep(time.Second)
return 0, errors.New("api1 failed")
}
我们可以借助 select 与 channel 完成对一组协程的并发控制。
代码语言:javascript复制package main
import (
"errors"
"fmt"
"time"
)
func main() {
errchan := make(chan error, 3)
retchan := make(chan struct{}, 3)
var ret1, ret2, ret3 int
now := time.Now()
go func() {
var err error
ret1, err = api1()
if err != nil {
errchan <- err
return
}
retchan <- struct{}{}
}()
go func() {
var err error
ret2, err = api2()
if err != nil {
errchan <- err
return
}
retchan <- struct{}{}
}()
go func() {
var err error
ret3, err = api3()
if err != nil {
errchan <- err
return
}
retchan <- struct{}{}
}()
// 阻塞等待被调接口出错或全部成功。
var err error
LOOP:
for {
select {
case err = <-errchan:
break LOOP
default:
if len(retchan) == 3 {
break
}
}
}
cost := time.Since(now)
fmt.Printf("err:%v cost:%vs ret1:%v ret2:%v ret3:%vn", err, cost, ret1, ret2, ret3)
}
运行输出:
代码语言:javascript复制err:api1 failed cost:1.0055006ss ret1:0 ret2:0 ret3:0
通过耗时 cost 为 1s 可见,并发调用下游接口,当接口 api1 失败时,不再继续等待其他接口的返回。
3.最早成功返回
如果并发调用多个接口时,只要有一个接口成功返回,其他接口无需再继续等待。即以最早成功返回的那个接口的结果为准,不再关心其他接口的返回。
我们还是假设接口 api1 的耗时最短,但是发生了失败。
代码语言:javascript复制func api1() (int, error) {
time.Sleep(time.Second)
return 0, errors.New("api1 failed")
}
接着我们要继续等待另外两个接口。因为接口 api2 比 api3 耗时短,且成功返回了,所以我们以 api2 返回的结果为准。
代码语言:javascript复制func api2() (int, error) {
time.Sleep(2*time.Second)
return 2, nil
}
我们使用 channel 接收被调接口的结束信号。
代码语言:javascript复制package main
import (
"fmt"
"time"
)
func main() {
errchan := make(chan error, 3)
var ret1, ret2, ret3 int
now := time.Now()
go func() {
var err error
ret1, err = api1()
errchan <- err
}()
go func() {
var err error
ret2, err = api2()
errchan <- err
}()
go func() {
var err error
ret3, err = api3()
errchan <- err
}()
var retnum int
var err error
// 阻塞等待直至有接口成功返回或全部结束。
LOOP:
for {
select {
case e := <-errchan:
retnum
if e != nil {
err = e
}
if e == nil || retnum == 3 {
break LOOP
}
}
}
cost := time.Since(now)
fmt.Printf("err:%v cost:%vs ret1:%v ret2:%v ret3:%vn", err, cost, ret1, ret2, ret3)
}
运行输出:
代码语言:javascript复制err:api1 failed cost:2.0001894ss ret1:0 ret2:2 ret3:0
通过耗时 cost 为 2s 可见,并发调用下游接口,当接口 api1 失败时,继续等待其他接口。当 api2 成功返回后,则直接结束主协程的阻塞。
4.小结
本文列举了不同业务场景下常见的并发协程管理方式:
- 全部返回
- 出错及时返回
- 最早成功返回
当然还有其他的并发模式,比如生产者消费者模型、发布订阅模型和控制并发数等,本文不再赘述。具体场景,具体分析。最终我们都可以借助 Go 为我们提供的一系列的同步原语完成对一组协程的控制。比如 sync 包下的 Mutex、RWMutex、WaitGroup、Once、Cond、Pool、Map,以及抽象层级更高的 channel、select、Context 等。除了标准库中提供的同步原语之外,Go 语言还在子仓库 sync 中提供了三种扩展原语 errgroup、semaphore 与 singleflight。
参考文献
1.6 常见的并发模式 - Go语言高级编程 Go 语言并发编程、同步原语与锁