Go语言常见并发模式

1.屏障模式:

屏障模式顾名思义就是一种屏障.用来阻塞goroutine直到聚合所有goroutine返回结果.可以使用通道来实现.该模式在并发应用中非常常见.例如.有一个微服务应用中的某个服务需要通过归并组合另外三个微服务返回的结果作为当前这个服务的结果.

屏障模式通过同步阻塞来实现屏障操作.直到组成该返回结果所依赖的其他一个或多个不同协程或者线程(微服务)的结果都被得到,在Go语言中.虽然标准库中提供了一些同步方法.如锁机制.但是更惯用的方法是通过通道来实现的.屏障模式使用场景如下.

1).多个网络请求并发.聚合结果.

2).粗粒度任务拆分并发执行.聚合结果.

同步屏障模式的目标如下.

1).组合来自一个或多个协程的同类型结果.

2).控制来自多个不同协程的数据传输通道.保证没有不一致的数据被返回.

示例:

go 复制代码
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

func main() {

    Barrier([]string{"https://www.baidu.com", "https://www.weibo.com"}...)
}

// 屏障模式响应结构体.
type BarrierResponse struct {
    Err    error
    Resp   string
    Status int
}

// 构造请求.
func doRequest(out chan<- BarrierResponse, url string) {
    res := BarrierResponse{}
    //设置Http客户端.
    client := http.Client{
       Timeout: time.Duration(20 * time.Second),
    }

    //执行请求.
    resp, err := client.Get(url)
    if resp != nil {
       res.Status = resp.StatusCode
    }
    if err != nil {
       res.Err = err
       out <- res
       return
    }
    byt, err := ioutil.ReadAll(resp.Body)
    defer resp.Body.Close()
    if err != nil {
       res.Err = err
       out <- res
       return
    }
    res.Resp = string(byt)
    //将获取的结果放入通道.
    out <- res
}

// 合并结果.
func Barrier(urls ...string) {
    requestNumber := len(urls)
    in := make(chan BarrierResponse, requestNumber)
    responses := make([]BarrierResponse, requestNumber)
    for _, url := range urls {
       go doRequest(in, url)
    }
    defer close(in)
    var hasError bool
    for i := 0; i < requestNumber; i++ {
       resp := <-in
       if resp.Err != nil {
          fmt.Println("Error:", resp.Err, resp.Status)
          hasError = true
       }
       responses[i] = resp
    }
    if !hasError {
       for _, resp := range responses {
          fmt.Println("Status:", resp.Status)
       }
    }
}

2.未来模式:

未来模式也称为承诺模式.常用在异步处理方面.未来模式采用的是一种"fire-and-forget"方式.意思是主进程不等子进程执行完成就直接返回.然后等到未来执行完的时候再去取结果.

未来模式中主goroutine不用等待子goroutine返回的结果.可以先去做其他的事情.等未来需要子goroutine结果的时候再来取.如果子goroutine还没有返回结果,则一直等待.

比如打算沏茶.那么就需要放茶叶 烧水.放茶叶 烧水这两个步骤相互之间没有依赖关系.是独立的.那么就可以同时做.但是最后沏茶这个步骤就需要放好茶叶烧好水才能进行.

示例:

csharp 复制代码
```
package main

import (
    "fmt"
    "time"
)

func main() {
    teaCh := putInTea()
    waterCh := boilingWater()
    fmt.Println("已经开始放茶叶.烧水")
    time.Sleep(2 * time.Second)
    fmt.Println("沏茶了.看看茶叶和水烧好了嘛")
    tea := <-teaCh
    water := <-waterCh
    fmt.Println("准备好了.可以沏茶", tea, water)
}

// 放茶叶.
func putInTea() <-chan string {
    vagetables := make(chan string)
    go func() {
       time.Sleep(5 * time.Second)
       vagetables <- "茶叶已经放入茶杯"
    }()
    return vagetables
}

// 烧水.
func boilingWater() <-chan string {
    water := make(chan string)
    go func() {
       time.Sleep(5 * time.Second)
       water <- "水已经烧开了."
    }()
    return water
}

3.管道模式:

管道模式也称为流水线模式.其模拟的就是现实世界中的生产流水线.以组装计算机为例.整条流水线可能有成百上千道工序.每道工序只负责自己的事情,最终经过一道道工序组装.就完成了生产.

示例:

go 复制代码
package main

import "fmt"

func main() {
    accessories := Buy(6)
    build := Build(accessories)
    pack := Pack(build)
    for s := range pack {
       fmt.Println(s)
    }
}

// 1.
func Buy(n int) <-chan string {
    out := make(chan string)
    go func() {
       defer close(out)
       for i := 0; i < n; i++ {
          out <- fmt.Sprint("配件", i)
       }
    }()
    return out
}

// 2.
func Build(in <-chan string) <-chan string {
    out := make(chan string)
    go func() {
       defer close(out)
       for v := range in {
          out <- "组装(" + v + ")"
       }
    }()
    return out
}

// 3.
func Pack(in <-chan string) <-chan string {
    out := make(chan string)
    go func() {
       defer close(out)
       for v := range in {
          out <- "打包(" + v + ")"
       }
    }()
    return out
}

1).流水线由一道道工序构成.每道工序通过通道把数据传递到下一个工序.

2).每道工序一般会对应一个函数.函数里有协程和通道,协程一般用于处理数据并把它放入通道中.每道工序会返回这个通道以供下一道工序使用.

3).最终要有一个组织者把这些工序串起来.就形成了一个完整的流水线.对于数据来说就是数据流.

4.扇出和扇入模式:

扇出是指多个函数可以从同一个通道读取数据.直到该通道关闭.扇入是指一个函数可以从多个输入中读取数据并继续执行.直到所有输入都关闭.扇出和扇入模式的方法是将输入通道多路复用到一个通道上.当所有输入关闭.该通道才关闭.

示例:

go 复制代码
package main

import (
    "fmt"
    "sync"
)

func main() {
    accessories := Buy(12)
    build1 := Build(accessories)
    build2 := Build(accessories)
    build3 := Build(accessories)
    merge := Merge(build1, build2, build3)
    pack := Pack(merge)
    for s := range pack {
       fmt.Println(s)
    }
}

func Merge(ins ...<-chan string) <-chan string {
    var wg sync.WaitGroup
    out := make(chan string)

    //把一个通道的数据放入到out中.
    p := func(in <-chan string) {
       defer wg.Done()
       for c := range in {
          out <- c
       }
    }

    wg.Add(len(ins))
    //扇入.需要启动多个goroutine用于处理通道的数据.
    for _, cs := range ins {
       go p(cs)
    }

    //等待所有输入的数据ins处理完.在关闭输出out.
    go func() {
       wg.Wait()
       close(out)
    }()
    return out
}

// 1.
func Buy(n int) <-chan string {
    out := make(chan string)
    go func() {
       defer close(out)
       for i := 0; i < n; i++ {
          out <- fmt.Sprint("配件", i)
       }
    }()
    return out
}

// 2.
func Build(in <-chan string) <-chan string {
    out := make(chan string)
    go func() {
       defer close(out)
       for v := range in {
          out <- "组装(" + v + ")"
       }
    }()
    return out
}

// 3.
func Pack(in <-chan string) <-chan string {
    out := make(chan string)
    go func() {
       defer close(out)
       for v := range in {
          out <- "打包(" + v + ")"
       }
    }()
    return out
}

5.协程池模式:

协程池模式是常见的并发设计模式.每个goroutine的初始内存消耗都较小.但当有大批量任务的时候.需要运行很多goroutine来处理.会给系统带来很大的内存开销和垃圾回收的压力.

示例:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    //设置缓冲大小.
    bufferSize := 100
    pool := NewWorkPool(bufferSize)
    workers := 4
    for i := 0; i < workers; i++ {
       pool.AddWorker()
    }
    var sum int32
    testFunc := func(i interface{}) {
       n := i.(int32)
       atomic.AddInt32(&sum, n)
    }

    var i, n int32
    n = 100
    for ; i < n; i++ {
       task := Task{
          i,
          testFunc,
       }
       pool.SendTask(task)
    }
    pool.Release()
    fmt.Println(sum)
}

// 任务处理器.
type TaskHandler func(interface{})

// 任务结构体.
type Task struct {
    Parm   interface{}
    Hanler TaskHandler
}

// 协程池接口.
type WorkerPoolImpl interface {
    //增加worker
    AddWorker()
    //发送任务
    SendTask(Task)
    //释放
    Release()
}

// 协程池.
type WorkPool struct {
    wg   sync.WaitGroup
    inCh chan Task
}

func (d *WorkPool) AddWorker() {
    d.wg.Add(1)
    go func() {
       for task := range d.inCh {
          task.Hanler(task.Parm)
       }
       d.wg.Done()
    }()
}

func (d *WorkPool) SendTask(task Task) {
    d.inCh <- task
}

func (d *WorkPool) Release() {
    close(d.inCh)
    d.wg.Wait()
}

// 实例化.
func NewWorkPool(buffer int) WorkerPoolImpl {
    return &WorkPool{
       inCh: make(chan Task, buffer),
    }
}

6.发布-订阅模式:

在软件架构中.发布-订阅是一种基于消息通知的并发设计模式.发布者发送消息.订阅者接收消息.消息的发送者不会将消息直接发送给特定的接收者.而是将发布的消息分为不同的类别.无需了解哪些订阅者可能存在.同样的.订阅者可以表达对一个或多个类别感兴趣.只接收感兴趣的消息.无须了解哪些发布者存在.

示例:

go 复制代码
package main

import (
    "fmt"
    "strings"
    "sync"
    "time"
)

func main() {
    publisher := NewPublisher(10000*time.Millisecond, 10)
    defer publisher.Close()
    all := publisher.Subscribe()
    golang := publisher.SubscribeTopic(func(v interface{}) bool {
       if s, ok := v.(string); ok {
          return strings.Contains(s, "golang")
       }
       return false
    })
    publisher.Publish("hello,world")
    publisher.Publish("hello,golang")

    time.Sleep(time.Second)
    publisher.Close()

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
       for msg := range all {
          _, ok := msg.(string)
          fmt.Println(ok)
       }
       wg.Done()
    }()

    go func() {
       for msg := range golang {
          v, ok := msg.(string)
          fmt.Println(ok)
          fmt.Println(v)
       }
       wg.Done()
    }()
    wg.Wait()
}

func NewPublisher(publishTimeOut time.Duration, buff int) *Publisher {
    return &Publisher{
       buffer:      buff,
       timeout:     publishTimeOut,
       subscribers: make(map[Subscriber]TopicFunc),
    }
}

// 发布者结构体.
type Publisher struct {
    //订阅者注册的地方.
    subscribers map[Subscriber]TopicFunc
    //订阅者缓冲区长度.
    buffer int
    //发送消息的超时时间.
    timeout time.Duration

    m sync.RWMutex
}

type (
    //订阅者通道
    Subscriber chan interface{}
    //主题函数
    TopicFunc func(v interface{}) bool
)

// 发布者订阅方法.
func (p *Publisher) Subscribe() Subscriber {
    return p.SubscribeTopic(nil)
}

// 发布者订阅主题.
func (p *Publisher) SubscribeTopic(topicFunc TopicFunc) Subscriber {
    ch := make(Subscriber, p.buffer)
    p.m.Lock()
    defer p.m.Unlock()
    p.subscribers[ch] = topicFunc
    return ch
}

// 删除某个订阅者.
func (p *Publisher) Delete(sub Subscriber) {
    p.m.Lock()
    defer p.m.Unlock()
    delete(p.subscribers, sub)
    close(sub)
}

// 发布者发送消息.
func (p *Publisher) Publish(v interface{}) {
    p.m.RLock()
    defer p.m.RUnlock()

    var wg sync.WaitGroup
    for sub, topic := range p.subscribers {
       wg.Add(1)
       go p.sendTopic(sub, topic, v, &wg)
    }
}

// 关闭发送者.删除所有订阅者.
func (p *Publisher) Close() {
    p.m.Lock()
    defer p.m.Unlock()
    for sub := range p.subscribers {
       delete(p.subscribers, sub)
       close(sub)
    }
}

// 发送主题.
func (p *Publisher) sendTopic(sub Subscriber, topic TopicFunc, v interface{}, wg *sync.WaitGroup) {
    defer wg.Done()
    if topic != nil && !topic(v) {
       return
    }
    select {
    case sub <- v:
    case <-time.After(p.timeout):
    }
}

语雀地址www.yuque.com/itbosunmian...?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

相关推荐
_Evan_Yao1 小时前
计算机大一新生如何选择方向(前端/后端/AI/运维)?
运维·前端·人工智能·后端
skilllite作者1 小时前
SkillLite Channel 与 Gateway 配置完全指南:Webhook、环境变量与桌面助手
ide·后端·前端框架
夕除2 小时前
spring boot 4
java·spring boot·后端
starsky762382 小时前
spring boot——前后端分离
java·spring boot·后端
战南诚2 小时前
Flask中的URL ——url_for() 与 自定义动态路由过滤器
后端·python·flask
折哥的程序人生 · 物流技术专研2 小时前
《Java面试85题图解版(三)》上篇:高阶架构设计篇
java·开发语言·后端·面试·职场和发展
Lee川11 小时前
LangChain 加持:后端 AI 流式对话的优雅实现
后端
子兮曰12 小时前
Bun v1.3.14 深度解析:Image API、HTTP/3、全局虚拟存储与五十项变革
前端·后端·bun
ltl12 小时前
Self-Attention:让序列自己看自己
后端