Go 的并发模式

176 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

类似于设计模式一样,为更好应对异步和并发的开发,对这类的场景进行抽象封装,以便提供一个统一的解决方案。

for select 模式

// for无限循环,或 for range 循环
for{
    select {
        // 使用一个 channel 控制
    }
}

// 例子1
for{
    select {
     case <- done:
      return
     case result <- s:
        // do something
    }
}
// 例子 2
for{
    select {
     case <- done:
      return
     default:
        // do something
    }
}

通过使用 for 循环+ select 的并发模式,只要有 case 分支满足条件就执行哪个,直到满足退出条件则退出循环。

如上面例子,done 分支主要用于退出循环,case result 分支主要用于接收循环的值,处理业务。也可以将此分支改为 default ,这样就会一直执行 default 分支的任务,直到 接收到 done 整个for 循环结束。

select timeout 模式

当需要请求第三方服务数据时,由于响应时间的差别,为防止因第三方服务问题导致一直等待响应,需要设置一个超时时间。那么可以使用 select timeout 模式

func doAction(){
    go func(){
     result <- getFromThirdApi()
    }()

    select {
        case v:= <- result:
            fmt.println("成功获取值")
        case <- time.After(5*time.second):
            fmt.println("网络超时")
    }
}

通过 time.After 函数设置一个超时时间,这样 select 分支就不会因异常造成select无限等待。

Pipeline 模式

Pipeline 模式即为流水线模式,将一件事拆解成多个工序,然后由多个工序组装完成最后的工作。每道工序通过 channel 把数据传递给下一工序。

func doFirst() <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for i := 0; i < 10; i++ {
            out <- spew.Sprint("配件", i)
        }
    }()
    return out
}
func doSecond(in <-chan string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for c := range in {
            out <- "组装(" + c + ")"
        }
    }()
    return out
}
func doThird(in <-chan string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for c := range in {
            out <- "打包(" + c + ")"
        }
    }()
    return out
}

func main() {
    da := doFirst()
    sa := doSecond(da)
    pa := doThird(sa)

    for p := range pa {
        fmt.Println(p)
    }
}
  • 设置三道工序,doFirst、 doSecond、doThird,每道工序都将最后结果存储在 channel 内,然后作为下一个工序的参数
  • 每道工序对应一个函数,函数内都有协程 goroutine 和 channel
  • 最后一个组织者将工序串起来,完成整个流水线。也是整个数据的数据流

Pipeline 是一种并发处理的模式,而不是Go 中的管道 channel。主要是将每个步骤都拆解成各自独立工序,每个工序都有各自的 channel ,并将channel 输出作为下一次工序的入参。第一道工序可能只有输出,最后工序可能只有输入。

Fade-In 扇入 和 Fade-out 扇出

  • 多个协程 goroutine 同时从同一 Channel 读取数据,直到关闭,称为 Fade-Out。
  • 多个协程 goroutine 从多个输入 channel 读取数据然后从同一个 channel 输出,直到所有的输入 channel 都关闭则将停止,称为 Fade-in
func merge(ins ...<-chan string) <-chan string {
    var wg sync.WaitGroup
    out := make(chan string)

    p := func(in <-chan string) {
        defer wg.Done()
        for c := range in {
            out <- c
        }
    }
    wg.Add(len(ins))
    // 启动多个协程,将多个输入 channel都发送到 out
    for _, cs := range ins {
        go p(cs)
    }

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



func main() {
    da := doFirst()

    sa1 := doSecond(da)
    sa2 := doSecond(da)
    sa3 := doSecond(da)
    sa := merge(sa1,sa2,sa3)

    pa := doThird(sa)

    for p := range pa {
        fmt.Println(p)
    }
}

增加 merge 工序,允许接收多个 channel,其中对每个 chan 都单独处理,并把处理结果都发送到变量 out。即多个协程并发,将多个chan 合并成一个。