欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Go并发控制WaitGroup的使用场景分析

程序员文章站 2022-03-07 08:54:05
1. 前言上一篇介绍了 go并发控制--channel使用channel来控制子协程的优点是实现简单,缺点是当需要大量创建协程时就需要有相同数量的channel,而且对于子协程继续派生出来的协程不方便...

1. 前言

上一篇介绍了 go并发控制--channel

使用channel来控制子协程的优点是实现简单,缺点是当需要大量创建协程时就需要有相同数量的channel,而且对于子协程继续派生出来的协程不方便控制。

2. 使用waitgroup控制

waitgroup,可理解为wait-goroutine-group,即等待一组goroutine结束。比如某个goroutine需要等待其他几个goroutine全部完成,那么使用waitgroup可以轻松实现。


2.1 使用场景

下面程序展示了一个goroutine等待另外两个goroutine结束的例子:

简单的说,上面程序中wg内部维护了一个计数器:

  • 启动goroutine前将计数器通过add(2)将计数器设置为待启动的goroutine个数。
  • 启动goroutine后,使用wait()方法阻塞自己,等待计数器变为0。
  • 每个goroutine执行结束通过done()方法将计数器减1。
  • 计数器变为0后,阻塞的goroutine被唤醒

其实waitgroup也可以实现一组goroutine等待另一组goroutine,这有点像玩杂技,很容出错,如果不了解其实现原理更是如此。实际上,waitgroup的实现源码非常简单。


2.2 信号量

信号量是unix系统提供的一种保护共享资源的机制,用于防止多个线程同时访问某个资源

可简单理解为信号量为一个数值:

  • 当信号量>0时,表示资源可用,获取信号量时系统自动将信号量减1;
  • 当信号量==0时,表示资源暂不可用,获取信号量时,当前线程会进入睡眠,当信号量为正时被唤醒;

1.3 waitgroup 数据结构

源码包中src/sync/waitgroup.go:waitgroup定义了其数据结构:

state1是个长度为3的数组,其中包含了state和一个信号量,而state实际上是两个计数器:

  • counter: 当前还未执行结束的goroutine计数器
  • waiter count: 等待goroutine-group结束的goroutine数量,即有多少个等候者
  • semaphore: 信号量

考虑到字节是否对齐,三者出现的位置不同,为简单起见,依照字节已对齐情况下,三者在内存中的位置如下所示:

waitgroup对外提供三个接口:

  • add(delta int): 将delta值加到counter中
  • wait(): waiter递增1,并阻塞等待信号量semaphore
  • done(): counter递减1,按照waiter数值释放相应次数信号量

下面分别介绍这三个函数的实现细节。

2.3.1 add () 方法

add()做了两件事,一是把delta值累加到counter中,因为delta可以为负值,也就是说counter有可能变成0或负值,所以第二件事就是当counter值变为0时,根据waiter数值释放等量的信号量,把等待的goroutine全部唤醒,如果counter变为负值,则panic.

add()伪代码如下:

2.3.2 wait()

wait()方法也做了两件事,一是累加waiter, 二是阻塞等待信号量

这里用到了cas算法保证有多个goroutine同时执行wait()时也能正确累加waiter。

2.3.3 done()

done()只做一件事,即把counter减1,我们知道add()可以接受负值,所以done实际上只是调用了add(-1)。

源码如下:

done()的执行逻辑就转到了add(),实际上也正是最后一个完成的goroutine把等待者唤醒的。

2.4 总结

简单说来,waitgroup通常用于等待一组“工作协程”结束的场景,其内部维护两个计数器,这里把它们称为“工作协程”计数器和“坐等协程”计数器,
waitgroup对外提供的三个方法分工非常明确:

  • add(delta int)方法用于增加“工作协程”计数,通常在启动新的“工作协程”之前调用;
  • done()方法用于减少“工作协程”计数,每次调用递减1,通常在“工作协程”内部且在临近返回之前调用;
  • wait()方法用于增加“坐等协程”计数,通常在所有”工作协

done()方法除了负责递减“工作协程”计数以外,还会在“工作协程”计数变为0时检查“坐等协程”计数器并把“坐等协程”唤醒。

需要注意

  • done()方法递减“工作协程”计数后,如果“工作协程”计数变成负数时,将会触发panic,这就要求add()方法调用要早于done()方法。
  • 也就是说代码中,如果调用done的次数多于add的次数会产生painc
  • 当“工作协程”计数多于实际需要等待的“工作协程”数量时,“坐等协程”可能会永远无法被唤醒而产生列锁,此时,go运行时检测到死锁会触发panic
  • add的添加的工作协程的数量,多于done调用的次数,则会出现panic
  • 当“工作协程”计数小于实际需要等待的“工作协程”数量时,done()会在“工作协程”计数变为负数时触发panic。
  • add()添加的工作协程个数小于done调用的次数,会出现panic


3. 总结

waitgroup控制子协程的方式很简单,且目的很明确,等待一组子协程执行完毕再执行主线程,但是当子协程里面有子协程,子协程里面有其他的子协程时,这种并不知道有多少个子协程的情况下使用waitgroup就很难,所以就需要****context**上场了

到此这篇关于go并发控制--waitgroup篇的文章就介绍到这了,更多相关go并发控制waitgroup内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!