• Go :WaitGroup简介与实践



    简介

    在Go语言中,sync.WaitGroup结构体对象用于等待一组线程的结束;WaitGroup是go并发中最常用的工具,我们可以通过WaitGroup来表达这一组协程的任务是否完成,以决定是否继续往下走,或者取任务结果;

    WaitGroup的结构体:

    type WaitGroup struct { 
        noCopy noCopy 
        state1 [3]uint32 
    }
    
    • 1
    • 2
    • 3
    • 4

    在sync.WaitGroup结构体对象中有三个方法,Add()、Done()、Wait()

    Add()方法主要为WaitGroup的等待数+1或者+n;

    func(*WaitGroup) Add()
    
    • 1
    • Add()方法内部计数器加上delta,delta可以是负数;
    • 如果内部计数器变为0,则Wait()方法会将处于阻塞等待的所有goroutine释放;
    • 如果计数器小于0,则调用panic()函数;
    • Add()方法加上正数的调用应在Wait()方法之前,否则Wait()方法可能只会等待很少的goroutine;
    • Add()方法在创建新的goroutine或者其它等待的事件之前调用;

    Done()方法 Done函数调用的也是Add函数,主要用于-1操作;

    func(wg *WaitGroup) Done()
    
    • 1
    • Done()方法会减少WaitGroup计数器的值,一般在goroutine的最后执行;

    Wait()方法 阻塞当前协程,直到等待数归为0才继续向下执行;

    func (wg *WaitGroup) Wait()
    
    • 1
    • Wait()方法会阻塞,知道WaitGroup计数器减为0.

    Add()、Done()、Wait()三者对比

    • 在三个方法中,使用Add()方法添加计数,使用Done()方法减掉一个计数,如果计数不为0,则会阻塞Wait()方法的运行;
    • 一个goroutine调用Add()方法来设定等待的goroutine的数量;
    • 每个被等待的goroutine在结束时调用Done()方法;
    • 在主goroutine里可以调用Wait()方法阻塞至所有goroutine结束;

    实践

    1.常见阻塞写法

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main(){
        for i := 0; i < 100 ; i++{
            go fmt.Println(i)
        }
        time.Sleep(time.Second)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    主线程为了等待goroutine都运行完毕,不得不在程序的末尾使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行。对于简单的代码,100个for循环可以在1秒之内运行完毕,time.Sleep() 也可以达到想要的效果。但是对于实际生活的大多数场景来说,1秒是不够的,并且大部分时候我们都无法预知for循环内代码运行时间的长短。这时候就不能使用time.Sleep() 来完成等待操作了。

    2.通过通道的写法

    func main() {
        c := make(chan bool, 100)
        for i := 0; i < 100; i++ {
            go func(i int) {
                fmt.Println(i)
                c <- true
            }(i)
        }
    
        for i := 0; i < 100; i++ {
            <-c
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    首先可以肯定的是使用管道是能达到我们的目的的,而且不但能达到目的,还能十分完美的达到目的。但是管道在这里显得有些大材小用,因为它被设计出来不仅仅只是在这里用作简单的同步处理,在这里使用管道实际上是不合适的。而且假设我们有一万、十万甚至更多的for循环,也要申请同样数量大小的管道出来,对内存也是不小的开销。

    3. 通过waitgroup的写法

    对于这种情况,go语言中有一个其他的工具sync.WaitGroup 能更加方便的帮助我们达到这个目的。WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器地值减为0。

    使用WaitGroup 将上述代码可以修改为:

    func main() {
        wg := sync.WaitGroup{}
        wg.Add(100)
        for i := 0; i < 100; i++ {
            go func(i int) {
                fmt.Println(i)
                wg.Done()
            }(i)
        }
        wg.Wait()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里首先把wg 计数设置为100, 每个for循环运行完毕都把计数器减一,主函数中使用Wait() 一直阻塞,直到wg为零——也就是所有的100个for循环都运行完毕。相对于使用管道来说,WaitGroup 轻巧了许多。

    三、注意事项

    1. 计数器不能为负值

    我们不能使用Add() 给wg 设置一个负值,否则代码将会报错:

    2. WaitGroup对象不是一个引用类型

    WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址:

    func main() {
        wg := sync.WaitGroup{}
        wg.Add(100)
        for i := 0; i < 100; i++ {
            go f(i, &wg)
        }
        wg.Wait()
    }
    
    // 一定要通过指针传值,不然进程会进入死锁状态
    func f(i int, wg *sync.WaitGroup) { 
        fmt.Println(i)
        wg.Done()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    小结

  • 相关阅读:
    Ubuntu20.0工作区(workspace)介绍,切换工作区方式和快捷键
    【负荷预测】布谷鸟(CS)算法优化BP神经网络的负荷及天气预测(Matlab代码实现)
    Intel System Flags and Fields in the EFLAGS Register
    3D人像手办定制业务再掀热潮,这一次有怎样的革新?(方法篇)
    【C++ • STL】一文带你走进string
    cocoeval 解析
    字节二面:TCP 为什么要三次握手?
    JVM——7.类加载与类加载器
    pyqt实现简易浏览器
    Java数据结构与算法(爬楼梯动态规划)
  • 原文地址:https://blog.csdn.net/zhanggqianglovec/article/details/127905793