069-Go 并发编程(三)

相信你已经对 goroutine 已经轻车熟路了,既然如此,我们来完成一个小任务。

1. 故事背景

背景是这样,我们的任务分成 3 个阶段。


这里写图片描述

  • 第一阶段:从 A 工厂采购三种不同材料。
  • 第二阶段:你需要带着三种材料,交给 B 工厂 3 个不同的车间去加工。
  • 第三阶段:等待 B 工厂所有材料加工完毕,生产出零件后,你才能带上所有零件去 C 工厂组装零件。

特别的,在上面的过程中,每个步骤的耗时都是不一样的。我们希望使用 goroutine 来模拟这个过程。

不妨假设,从 A 采购的 3 种材料分别为 X0, Y0, Z0. 经过 B 加工后,X0 变成了 X,Y0 变成了 Y,Z0 变成了 Z. 最后拿到了 X, Y, Z 后,找到 C 进行组装,得到 XYZ 产品。

我们用伪代码描述上面的过程:

x, y, z := A()     // 采购 x, y, z,初始的值是 "X0", "Y0", "Z0"
go B1(&x)          // 加工 x
go B2(&y)          // 加工 y
go B3(&z)          // 加工 z
wait B1, B2, B3    // 等待三个车间全部完成加工
p := C(&x, &y, &z) // 组装

2. 程序

2.1 版本一

package main

import (
    "fmt"
    "time"
)

func A() (string, string, string) {
    time.Sleep(1 * time.Second)
    return "X0", "Y0", "Z0"
}

func B1(x *string) {
    time.Sleep(1 * time.Second)
    *x = "X"
}

func B2(y *string) {
    time.Sleep(2 * time.Second)
    *y = "Y"
}

func B3(z *string) {
    time.Sleep(3 * time.Second)
    *z = "Z"
}

func C(x, y, z string) string {
    time.Sleep(1 * time.Second)
    return x + y + z
}

func elapse() func() {
    now := time.Now()
    return func() {
        fmt.Printf("elapse:%.3f ms\n", 1000*time.Since(now).Seconds())
    }
}

func main() {
    defer elapse()()
    x, y, z := A()
    go func() {
        B1(&x)
    }()
    go func() {
        B2(&y)
    }()
    go func() {
        B3(&z)
    }()
    p := C(x, y, z)
    fmt.Printf("produce:%s\n", p) // Output: X0Y0Z0
}


这里写图片描述
图1 未进行同步的程序

上面这段程序明显是不符合预期的。因为工厂 C 根本没有等待 B 全部完成加工,就进行生产了。聪明的同学应该立即想到了使用 channel 进行同步的方法。

2.2 版本二

func main() {
    defer elapse()()
    x, y, z := A()

    wait := make(chan struct{}, 3) // 使用大小为 3 的 channel 进行同步
    go func() {
        B1(&x)
        wait <- struct{}{}
    }()
    go func() {
        B2(&y)
        wait <- struct{}{}
    }()
    go func() {
        B3(&z)
        wait <- struct{}{}
    }()

    for i := 0; i < 3; i++ {
        <-wait
    }
    p := C(x, y, z)
    fmt.Printf("produce:%s\n", p)
}


这里写图片描述
图2 使用 channel 进行同步

3. WaitGroup

你以为这样就结束了吗?如果只是讲一下利用 channel 进行 goroutine 同步,那确实没什么好讲的。这里我们介绍另一种试进行 goroutine 同步,使用 go 标准库自带的 package,sync 包。

来看一下最终的程序:

func main() {
    defer elapse()()
    x, y, z := A()
    // 你得事先 import "sync"
    var wg sync.WaitGroup

    wg.Add(3)
    go func() {
        B1(&x)
        wg.Done()
    }()
    go func() {
        B2(&y)
        wg.Done()
    }()
    go func() {
        B3(&z)
        wg.Done()
    }()

    wg.Wait()
    p := C(x, y, z)
    fmt.Printf("produce:%s\n", p)
}

这个程序同样可以达到图 2 中那样的效果。接下来我们分析一下,这段程序是怎么工作的,以及 sync.WaitGroup 是干啥使的。

  • 首先定义了一个 wg 变量,然后将 wg 变量的内置计数器设置为 3
  • 开启 3 个 goroutine 后,主协程会阻塞在 wg.Wait() 上,走到 wg 变量内置计数变为 0
  • 每个协程结束后,会调用 wg.Done(),wg 内置计数会减 1.

明白了它的原理后,你甚至可以使用 channel 粗糙的实现一个类似 sync.WaitGroup 类似的功能。当然了,这不能做到像 WaitGroup 这样的灵活度,不过这里我们就不深究了。

3.1 WaitGroup

A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.

WaitGroup 的作用是等待一组协程结束。主协程通过调用 Add 方法来设置要等待的协程数量。每个协程在运行结束后,都需要调用 Done 方法。Wait 方法可以用来阻塞主协程,直到所有的协程结束。

从文档上看,WaitGroup 提供的三个方法 Add, Done, Wait 在我们上面的例子中已经全部用到了,用法也非常简单。

4. 总结

  • 掌握协程同步的方法
  • 掌握 WaitGroup 包

当然了,Sync 包除了 WaitGroup 这样的类型外,还有诸多其它类型,以后我们还会介绍。

猜你喜欢

转载自blog.csdn.net/q1007729991/article/details/80953413