【Java&Go并发编程系列】4.等待一组并发任务完成——CountDownLatch VS sync.WaitGroup

说明:Java & Go 并发编程序列的文章,根据每篇文章的主题或细分标题,分别演示 Java 和 Go 语言当中的相关实现。更多该系列文章请查看:Java & Go 并发编程系列

本文介绍 Java 和 Go 语言中如何实现并发编程中等待一组并发任务完成的场景。

代码场景:假设有一道题目,同时分发给10个同学来完成,所有同学完成之后,老师公布答案。

「Java」CountDownLatch

使用 CountDownLatch 等待一组并发任务的完成,包含如下三要素:

  • 一个初始值,即定义需要等待的任务的数目
  • await() 需要等待并发任务先完成的线程调用
  • countDown(),每个任务在完成的时候调用
public static void main(String[] args) throws InterruptedException {

    // 声明初始计数为10
    CountDownLatch countDownLatch = new CountDownLatch(10);

    // 起10个线程模拟10个同学做题目
    for (int i = 0; i < 10; i++) {
        Thread t = new Thread(() -> {
            // 生成从1-10的随机整数,模拟做题目花费的时间
            int randomSecond = ThreadLocalRandom.current().nextInt(10) + 1;
            try {
                TimeUnit.SECONDS.sleep(randomSecond);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("第%s号同学用时%ds完成了题目
", Thread.currentThread().getName(), randomSecond);
            // 计数减1
            countDownLatch.countDown();
        });
        // 设置线程名称,用于打印对应的同学编号
        t.setName(String.valueOf(i));
        t.start();
    }

    // 阻塞直到 CountDownLatch 的计数为0
    // 注意这里是 await 方法, 不是 wait 方法
    countDownLatch.await();
    System.out.printf("所有的同学都完成了题目,老师公布答案
");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

输出结果:

第2号同学用时2s完成了题目
第9号同学用时2s完成了题目
第8号同学用时2s完成了题目
第3号同学用时2s完成了题目
第5号同学用时4s完成了题目
第6号同学用时6s完成了题目
第1号同学用时8s完成了题目
第0号同学用时9s完成了题目
第4号同学用时9s完成了题目
第7号同学用时9s完成了题目
所有的同学都完成了题目,老师公布答案
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

「Go」sync.WaitGroup

使用 sync.WaitGroup 等待一组并发任务的完成,包含如下三要素:

  • Add(),传入一个 int 类型的参数,增加计数器的数值
  • Wait(),需要等待并发任务先完成的 goroutine 调用
  • Done(),每个任务在完成的时候调用
func main() {
	// sync.WaitGroup 是开箱即用的类型
	var wg sync.WaitGroup
	// 初始化计数器的值为10
	wg.Add(10)
	// 启用10个 goroutine 用于并发执行 go 函数 
	// goroutine,即用户级线程,也称协程,是 Go 语言并发编程模型中重要的三个元素 GPM 之一
	for i := 0; i < 10; i++ {
		go func(i int) {
			randomSecond := rand.Intn(10) + 1
			time.Sleep(time.Second * time.Duration(randomSecond))
			fmt.Printf("第%d号同学用时%ds完成了题目
", i, randomSecond)
			// 计数器减1
			wg.Done()
		}(i)
	}
	// 阻塞直到所有并发任务执行完成
	wg.Wait()
	fmt.Printf("所有的同学都完成了题目,老师公布答案
")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

为了让程序的逻辑更严密,可以使用 defer 语句来保证 wg.Done() 一定被执行。

func main() {
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func(i int) {
			// defer 语句用于延迟执行代码,在函数正常或异常结束的时刻执行
			// 类似于 Java 的 finally
			defer wg.Done()
			randomSecond := rand.Intn(10) + 1
			time.Sleep(time.Second * time.Duration(randomSecond))
			fmt.Printf("第%d号同学用时%ds完成了题目
", i, randomSecond)
		}(i)
	}
	wg.Wait()
	fmt.Printf("所有的同学都完成了题目,老师公布答案
")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

输出结果:

第2号同学用时1s完成了题目
第0号同学用时1s完成了题目
第6号同学用时2s完成了题目
第1号同学用时2s完成了题目
第8号同学用时6s完成了题目
第4号同学用时7s完成了题目
第5号同学用时8s完成了题目
第9号同学用时8s完成了题目
第7号同学用时9s完成了题目
第3号同学用时10s完成了题目
所有的同学都完成了题目,老师公布答案
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

拓展

「Java」CyclicBarrier

CyclicBarrier 可传入另一个 Runnable 对象,在所有线程都到达集合点后,将运行该 Runnable 对象,使用场景更广泛。

public static void main(String[] args) {

    // 声明10参与者和结束时要运行的 Runnable 对象
    // 这里只是打印了一行文字
    CyclicBarrier cyclicBarrier = new CyclicBarrier(10,
            () -> System.out.printf("所有的同学都完成了题目,老师公布答案
")
    );

    // 起10个线程模拟10个同学做题目
    for (int i = 0; i < 10; i++) {
        Thread t = new Thread(() -> {
            // 生成从1-10的随机整数,模拟做题目花费的时间
            int randomSecond = ThreadLocalRandom.current().nextInt(10) + 1;
            try {
                TimeUnit.SECONDS.sleep(randomSecond);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("第%s号同学用时%ds完成了题目
", Thread.currentThread().getName(), randomSecond);
            try {
                // 阻塞直到所有参与者都调用 await 方法
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
        // 设置线程名称
        t.setName(String.valueOf(i));
        t.start();
    }

    System.out.printf("主线程执行结束
");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

输出结果:

主线程执行结束
第1号同学用时1s完成了题目
第5号同学用时1s完成了题目
第0号同学用时2s完成了题目
第3号同学用时4s完成了题目
第9号同学用时4s完成了题目
第2号同学用时5s完成了题目
第6号同学用时5s完成了题目
第4号同学用时6s完成了题目
第7号同学用时8s完成了题目
第8号同学用时10s完成了题目
所有的同学都完成了题目,老师公布答案
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

更多该系列文章请查看:Java & Go 并发编程系列

原文地址:https://www.cnblogs.com/xiami2046/p/13936911.html