for select
for select 循环模式非常常见,之前也介绍过,它一般和channel组合完成任务,代码格式如下:
for { // for 无限循环,或者for range
select {
// 通过一个channel控制
}
}
// for select 无限循环
for {
select {
case <- done:
return
default :
// 执行具体任务
}
}
// for range select 有限循环
for _, s := range []int{} {
select {
case <-done:
return
case resultCh <- s:
}
}
- 第一种for + select 多路复用的并发模式,那个case满足要求执行哪个,直到满足一定条件退出for循环。这种模式会一直执行default语句中的任务,直到channel被关闭为止
- 第二种模式是for range select 有限循环,一般用于把可以迭代的内容发送到channel上。这种模式也会有一个done channel,用于退出当前for循环,另一个resultCh channel用于接收for range 循环的值,这些值通过resultCh 可以传递给其他调用者。
select timeout模式
假如需要访问服务器获取数据,因为网络的响应时间不一样,为保证程序的质量,不可能一直等待,所以需要设置一个超时时间,这时候可以使用**select timeout**模式。
func main(){
result := make(chan string)
go func(){
// 模拟网络访问
time.Sleep(8 * time.Second)
result <- "服务端结果"
}()
select {
case v:= <- result:
fmt.Println(v)
case <- time.After(5 * time.Second):
fmt.Println("网络访问超时了")
}
}
select timeout 模式的核心在于通过 **time.After**函数设置一个超时时间,防止因为异常造成select语句的无限等待。**小提示**:如果可以使用Context的WithCancel函数超时取消,要优先使用。
Pipiline模式
**Pipeline模式也称为流水线模式,模拟的就是现实世界中的流水线生产**。从技术上看,每一道工序的输出,就是下一道工序的输入,在工序之间传递的东西就是数据,这种模式称为流水线模式,而传递的数据称为数据流。
以组装手机为例,讲解流水线模式的使用。假设一条组装手机的流水线有3道工序,分别是配件采购、配件组装、打包成品。相对工序2来说,工序1是生产者,工序3是消费这。相对工序1来说,工序2是消费者。相对工序3来说,工序2是生产者。
// 工序1采购
func buy(n int) <- chan string {
out := make(chan string)
go func() {
defer close(out)
for i:=1; i <= n ; i++ {
out <- fmt.Println("配件", 1)
}
}()
return out
}
// 工序2组装
func build(in <- chan string) <- chan string {
out := make(chan string)
go func(){
defer close(out)
for c := range in {
out <- "组装(" + c + ")"
}
}()
return out
}
// 工序3打包
func page(in <- chan string) <- chan string {
out := make(chan string)
go func(){
defer close(out)
for c := range in {
out <- "打包(" + c + ")"
}
}()
return out
}
func main() {
coms := buy(10)
phones := build(coms)
packs := pack(phones)
for p:= range packs {
fmt.Println(p)
}
}
// 输出结果
打包(组装(配件1))
打包(组装(配件2))
打包(组装(配件3))
打包(组装(配件4))
打包(组装(配件5))
打包(组装(配件6))
打包(组装(配件7))
打包(组装(配件8))
打包(组装(配件9))
打包(组装(配件10))
上述例子中,我们可以总结出一个流水线模式的构成:
- 流水线由一道道工序构成,每到工序通过channel把数据传递到下一个工序
- 每道工序一般都会对应一个函数,函数里有协程和channel,协程一般用于处理数据并把它放入一个channel中,整个函数会返回这个channnel以供下一道工序使用
- 最终要有一个组织者把这些工序串起来,这样就形成了一个完整的流水线,对于数据来说就是数据流
扇入和扇出模式
手机流水线经过一段时间运转,组织者发现产能提不上去,经过调研分析,瓶颈在工序2配件组装。工序2过慢,导致工序1配件采购速度不得不下降,下游工序3没什么事情做,不得不闲着,这就是整条流水线产能低下的原因。为了提升产能,组织者决定对工序2增加两班人手。人手增加后,整条流水线示意图如下:
改造后的流水线示意图可以看到,工序2有工序2-1、2-2、2-3三组人手,工序1采购的配件会被工序2的3班人手同时组装,这三班人手组装好的手机会同时传给merge组件汇聚,然后再传给工序3打包。这个流程中,会产生两种模式:**扇出和扇入**。
- 红色的部分是扇出,对于工序1来说,它同时为工序2的三班人手传递数据,已工序1为中心,三条传递数据的线发散出去,就像一把打开的扇子一样,所以叫扇出
- 蓝色的部分是扇入,对于merge组件来说,它同时接收工序2三班人手传递的数据进行汇聚,然后传给工序3.已merge组件为中心,三条传递数据的线汇聚到merge组件,也像一个打开的扇子一样,所以叫扇入
Tips:扇出和扇入都像一把打开的扇子,因为数据传递的方向不同,所以叫法也不一样,扇出的数据流是发散传递出去,是输出流;扇入的数据流是汇聚进来,是输入流。
// 扇入函数(组件),把多个channel中的数据发送到一个channel中
func merge(ins ...<-chan string) <- chan string {
var wg sync.WaitGroup
out := make(chan string)
// 把一个channel中的数据发送到out中
p := func(in <- chan string) {
defer wg.Done()
for c := range in {
out <- c
}
}
wg.Add(len(ins))
// 扇入,需要启动多个goroutine用于处理多个channel中的数据
for _, cs := range ins {
go p(cs)
}
// 等待所有输入的数据ins处理完,再关闭输出out
go func() {
wg.Wait()
close(out)
}()
return out
}
新增的merge函数的核心逻辑就是对输入的每个channel使用单独的协程处理,并将每个携程处理的结果都发送到变量out中,达到扇入的目的。总结起来就是通过多个协程并发,把多个channel 合成一个。
在整个手机组装流水线中,merge函数非常小,而且和业务无关,不能当做一道工序,所以管它叫**组件**。该merge组件是可以复用的,流水线中的任何工序需要扇入的时候,都可以使用merge组件。
Tips:改造新增了merge函数,其他函数保持不变,符合开闭原则。开闭原则规定“软件中的对象(类、模块、函数等等)应该对于扩展是开放的,但是对于修改是封闭的”。
// 流水线组织者main函数如何使用扇出和扇入并发模式
func main() {
coms := buy(100)
// 同时调用3次build函数,也就是为工序2增加人手,然后通过merge函数将三个channel汇聚成一个,然后传给pack函数打包
phones1 := build(coms)
phones2 := build(coms)
phones3 := build(coms)
// 汇聚三个channel成一个
phones := merge(phones1, phones2, phones3)
packs := pack(phones)
// 输出
for p := range packs {
fmt.Println(p)
}
}
通过扇出和扇入模式,整条流水线就被扩充好了,大大提高了生产效率。因为已经有了通用的扇入组件merge,所以整条流水线中任何需要扇出、扇入提高性能的工序,都可以复用merge组件做扇入,并且不用做任何隔离。
Futures模式
Pipeline流水线模式中的工序是相互依赖的,上一道工序做完,下一道工序才能开始。但是在我们实际需求中,也有大量的任务之间相互独立、没有依赖,所以为了提高性能,这些独立的任务就可以并发执行。
Futures模式可以理解为**未来模式**,主协程不用等待子协程返回的结果,可以先去做其他事情,等未来需要子协程结果的时候再来取,如果子协程没有返回结果,就一直等待。
// 洗菜和烧水是两个相互独立的任务可以一起做,所以可以通过开启协程的方式,实现同时做的功能。当任务完成后,结果会通过channel返回。
// 洗菜
func washVegetables() <- chan string {
vegetables := make(chan string)
go func(){
time.Sleep(5 * time.Second)
vegetables <- "洗好的菜"
}()
return vegetables
}
// 烧水
func boilWater() <- chan string {
water := make(chan string)
go func(){
time.Sleep(5 * time.Second)
water <- "水烧好了"
}()
return water
}
func main(){
vagetablesCh := washVegetables() // 洗菜
waterCh := boilWater() // 烧水
fmt.Println("已经安排洗菜和烧水了,休息一下")
time.Sleep(3 * time.Second)
fmt.Println("要做反了,看看菜和水好了吗")
vegetables := <- vegetables
water := <-waterCh
fmt.Println("准备好了,可以做饭了", vegetables, water)
}
Futures模式下的协程和普通协程最大区别是可以返回结果,而这个结果会在未来的某个时间点使用。所以在未来获取这个结果的时候的操作必须是阻塞的操作,要一直等到获取结果为止。
如果大的任务可以拆解为一个个独立并发执行的小任务,并且可以通过这些小任务的结果得出最终大任务的结果,就可以使用Future模式。
小结:并发模式和设计模式很相似,都是对现实场景的抽象封装,以便提供一个统一的解决方案。但是和设计模式不同的是,并发模式更专注于异步和并发。