【Go高效并发模式】

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是生产者。

image-20211215225852832

// 工序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))

上述例子中,我们可以总结出一个流水线模式的构成:

  1. 流水线由一道道工序构成,每到工序通过channel把数据传递到下一个工序
  2. 每道工序一般都会对应一个函数,函数里有协程和channel,协程一般用于处理数据并把它放入一个channel中,整个函数会返回这个channnel以供下一道工序使用
  3. 最终要有一个组织者把这些工序串起来,这样就形成了一个完整的流水线,对于数据来说就是数据流

扇入和扇出模式

​ 手机流水线经过一段时间运转,组织者发现产能提不上去,经过调研分析,瓶颈在工序2配件组装。工序2过慢,导致工序1配件采购速度不得不下降,下游工序3没什么事情做,不得不闲着,这就是整条流水线产能低下的原因。为了提升产能,组织者决定对工序2增加两班人手。人手增加后,整条流水线示意图如下:

image-20211216000632613

​ 改造后的流水线示意图可以看到,工序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模式。

小结:并发模式和设计模式很相似,都是对现实场景的抽象封装,以便提供一个统一的解决方案。但是和设计模式不同的是,并发模式更专注于异步和并发。


  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本书作者带你一步一步深入这些方法。你将理解 Go语言为何选定这些并发模型,这些模型又会带来什么问题,以及你如何组合利用这些模型中的原语去解决问题。学习那些让你在独立且自信的编写与实现任何规模并发系统时所需要用到的技巧和工具。 理解Go语言如何解决并发难以编写正确这一根本问题。 学习并发与并行的关键性区别。 深入到Go语言的内存同步原语。 利用这些模式中的原语编写可维护的并发代码。 将模式组合成为一系列的实践,使你能够编写大规模的分布式系统。 学习 goroutine 背后的复杂性,以及Go语言的运行时如何将所有东西连接在一起。 作者简介 · · · · · · Katherine Cox-Buday是一名计算机科学家,目前工作于 Simple online banking。她的业余爱好包括软件工程、创作、Go 语言(igo、baduk、weiquei) 以及音乐,这些都是她长期的追求,并且有着不同层面的贡献。 目录 · · · · · · 前言 1 第1章 并发概述 9 摩尔定律,Web Scale和我们所陷入的混乱 10 为什么并发很难? 12 竞争条件 13 原子性 15 内存访问同步 17 死锁、活锁和饥饿 20 确定并发安全 28 面对复杂性的简单性 31 第2章 对你的代码建模:通信顺序进程 33 并发与并行的区别 33 什么是CSP 37 如何帮助你 40 Go语言并发哲学 43 第3章 Go语言并发组件 47 goroutine 47 sync包 58 WaitGroup 58 互斥锁和读写锁 60 cond 64 once 69 池 71 channel 76 select 语句 92 GOMAXPROCS控制 97 小结 98 第4章 Go语言并发模式 99 约束 99 for-select循环103 防止goroutine泄漏 104 or-channel 109 错误处理112 pipeline 116 构建pipeline的最佳实践 120 一些便利的生成器 126 扇入,扇出 132 or-done-channel 137 tee-channel 139 桥接channel模式 140 队列排队143 context包 151 小结 168 第5章 大规模并发 169 异常传递169 超时和取消 178 心跳 184 复制请求197 速率限制199 治愈异常的goroutine 215 小结 222 第6章 goroutine和Go语言运行时 223 工作窃取223 窃取任务还是续体 231 向开发人员展示所有这些信息 240 尾声 240 附录A 241

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值