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内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!