CountDownLatch和WaitGroup

本文通过火锅食材采购实例,详细对比了Go语言中的WaitGroup与Java中的CountDownLatch,介绍了这两种并发控制工具的基本用法及注意事项。
引言

最近开始学习Go语言,前两天看到了Go语言中的WaitGroup,稍微看了一下用法,咋一看这和我平时熟悉的java中的CountDownLatch的用法很像啊。

CountDownLatch

咱先说说啥是CountDownLatch,它是一个同步器,是JAVA并发包下的一个常用的并发工具类,一般使用在一个线程在等待其他几个线程完成后再进行下一步操作时使用的。举个栗子:我们现在在家想吃火锅(广东话:打边炉)。我们是不是要先买肉,买蔬菜,买饮料等等。我们只有买齐了材料才可以围在桌前一起吃火锅。但是如果买这么多材料让我自己一个人去做,那岂不是要很长时间才能搞定。吃火锅肯定不是一个人吃的嘛(这样太寂寞了),所以我们可以让A去买肉,让B去买蔬菜,让C去饮料,我就在家等着他们回来,就可以开锅吃火锅了,哇哈哈哈哈(这个栗子可能举得不是很好,希望能明白我的意思)下面来看看我们的代码,也许你就懂了。

一个人去买所有材料
/**
 * 以下写是三个相似的方法,没有抽成一个方法
 * 主要还是为了模拟实际情况中
 * 我们一般在不用线程中调用不同的业务方法
 */
public static void buyMeat(String who) {
    System.out.println(who + "去买肉了!!");
    try {
        // 模拟耗时操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好肉了!!");
}

public static void buyVegetables(String who) {
    System.out.println(who + "去蔬菜了!!");
    try {
        // 模拟耗时操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好蔬菜了!!");
}

public static void buyDrink(String who) {
    System.out.println(who + "去买饮料了!!");
    try {
        // 因为饮料很重,所以花费的时间比较长
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好饮料了!!");
}

public static void main(String[] args) throws Exception {
    long start = System.currentTimeMillis();
    buyMeat("我");
    buyVegetables("我");
    buyDrink("我");
    System.out.println("耗时:" + (System.currentTimeMillis() - start) + "毫秒");

    System.out.println("材料都买好了!");
    System.out.println("开始吃火锅!");
}

输出结果:

我去买肉了!!
我买好肉了!!
我去蔬菜了!!
我买好蔬菜了!!
我去买饮料了!!
我买好饮料了!!
耗时:4002毫秒
材料都买好了!
开始吃火锅!

从上面可以发现,如果一个人去完成所有的事情是要很长时间,程序是顺序执行的,所以总耗时是执行所有方法耗时的和(所有买材料方法的耗时)。如果我们可以多个人去购买材料,这样耗时就可以大幅度减少。但是有个问题就是,我们怎么确定材料都买好了呢,这就要用到同步器,买好了材料都告诉同步器,我买好了材料。我工作中比较常用的同步器是CountDownLatch,当然还有其他的同步器比如说:Semaphore,Barrier等等

多个人去买材料(多线程操作):
/**
 * 创建一个有三个线程的线程池
 */
private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(3);

/**
 * 以下写上个相识的方法
 * 主要还是为了模拟实际情况中
 * 我们一般在不用线程中调用不同的业务方法
 */
public static void buyMeat(String who) {
    System.out.println(who + "去买肉了!!");
    try {
        // 模拟耗时操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好肉了!!");
}

public static void buyVegetables(String who) {
    System.out.println(who + "去蔬菜了!!");
    try {
        // 模拟耗时操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好蔬菜了!!");
}

public static void buyDrink(String who) {
    System.out.println(who + "去买饮料了!!");
    try {
        // 因为饮料很重,所以花费的时间比较长
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好饮料了!!");
}

public static void main(String[] args) throws Exception {
    long start = System.currentTimeMillis();
    final CountDownLatch countDownLatch = new CountDownLatch(3);

    //安排A B C各自去买东西
    EXECUTOR_SERVICE.execute(new Runnable() {
        public void run() {
            buyMeat("A");
            countDownLatch.countDown();
        }
    });

    EXECUTOR_SERVICE.execute(new Runnable() {
        public void run() {
            buyVegetables("B");
            countDownLatch.countDown();
        }
    });

    EXECUTOR_SERVICE.execute(new Runnable() {
        public void run() {
            buyDrink("C");
            countDownLatch.countDown();
        }
    });

    //我在家等着
    countDownLatch.await();

    System.out.println("耗时:" + (System.currentTimeMillis() - start) + "毫秒");

    System.out.println("材料都买好了!");
    System.out.println("开始吃火锅!");
}

输出结果:

A去买肉了!!
B去蔬菜了!!
C去买饮料了!!
B买好蔬菜了!!
A买好肉了!!
C买好饮料了!!
耗时:2005毫秒
材料都买好了!
开始吃火锅!

可以看到这次的耗时减少差不多2000毫秒,其实整个操作下来的主要耗时在C买饮料的操作上,买饮料的操作耗时需要2000毫秒,其他买肉买蔬菜的操作都是1000毫秒。看到这里你应该能看懂CountDownLatch大概是干嘛用的,大概是怎么用的了吧。

使用CountDownLatch需要注意的点

1、使用CountDownLatch的时候,记得在每个线程调用完成业务方法之后要调用CountDownLatch的countDown()方法。
CountDownLatch内部是通过一个计数器来实现的,每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务。
计数器的初始值就是new CountDownLatch(x)时传入的x值,x应设置为你线程调用的数量,每个线程完成一个操作后应该调用countDown()方法,进行计数器的减1操作。
在实际工作当中,根据业务场景,我一般会把countDown()方法写在finally中,因为即便是业务方法调用出现异常,也能正常的countDown,这样不会使得CountDownLatch一直在await()等待。
2、为了避免CountDownLatch一直在await()等待,我在工作中一般不会直接使用await()方法,一般使用其重载的方法
await(long timeout, TimeUnit unit),第一个参数是等待的时长,第二个参数是时间单位,比如说:等待5秒 countDownLatch.await(5, TimeUnit.SECONDS);

/*
* @param timeout the maximum time to wait
* @param unit the time unit of the {@code timeout} argument
* @return {@code true} if the count reached zero and {@code false}
*         if the waiting time elapsed before the count reached zero
* @throws InterruptedException if the current thread is interrupted
*         while waiting
*/
public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

以上就是我平时CountDownLatch的一些总结,如果那些地方有错的,还希望各位大神指正指正。谢谢啦!

WaitGroup

WaitGroup是我在学习Go语言时在学到Channel的时候看到的,WaitGroup给我的第一感觉就是它和CountDownLatch十分的相识。它是等待各个goruntine协程完成操作后,再进行下一步操作。
还是用买火锅材料的例子来看一下Go的实现的代码。这次直接演示A B C都去买材料的情况

package main

import (
	"fmt"
	"sync"
	"time"
)

func buyMeat(who string) {
	fmt.Println(who, "去买肉了!!!")
	//睡眠一秒
	time.Sleep(time.Second)
	fmt.Println(who, "买好肉了!!!")
}

func buyVegetables(who string) {
	fmt.Println(who, "去蔬菜了!!!")
	//睡眠一秒
	time.Sleep(time.Second)
	fmt.Println(who, "买好蔬菜了!!!")
}

func buyDrink(who string) {
	fmt.Println(who, "去买饮料了!!!")
	//睡眠两秒
	time.Sleep(2 * time.Second)
	fmt.Println(who, "买好饮料了!!!")
}

func main() {
	start := time.Now().UnixNano() / 1e6

	wg := sync.WaitGroup{}
	wg.Add(3)

	//安排A B C去买材料
	go func() {
		buyMeat("A")
		wg.Done()
	}()

	go func() {
		buyVegetables("B")
		wg.Done()
	}()

	go func() {
		buyDrink("C")
		wg.Done()
	}()

	wg.Wait()
	end := time.Now().UnixNano() / 1e6
	fmt.Println("耗时:", end-start, "毫秒")
	fmt.Println("材料买好了!!!")
	fmt.Println("吃火锅啦!!!")
}

输出结果:

C 去买饮料了!!!
A 去买肉了!!!
B 去蔬菜了!!!
A 买好肉了!!!
B 买好蔬菜了!!!
C 买好饮料了!!!
耗时: 2001 毫秒
材料买好了!!!
吃火锅啦!!!

来来来,即便你没学习过Go,你是否也会感觉WaitGroup和CountDownLatch的用法是不是很像。我觉得是很像的,感觉JAVA程序猿上手这个WaitGroup还是比较容易的。
WaitGroup通过设定计数器,每个写个goruntine协程在退出前进行Done()操作递减1次,直到为0时解除阻塞。(CountDownLatch上面也说到也是进行计数器减1操作,有没有很像(っ•̀ω•́)っ✎⁾⁾)

WaitGroup注意的点

1、要保证每次操作Done的WaitGroup对象是同一个对象
2、WaitGroup.Add实现了院子操作,但是还是建议在goruntine外进行累加计数器,以避免Add操作未执行,Wait就已经退出了

总结

最近在学习Go语言,一边学习一边对比着JAVA,感觉整个过程也是挺有趣的,因为接触Go时间不长现在还在一个初步学习的阶段,以上文章中哪里有误的,希望大家可以指正我一下,相互学习,谢谢!

### 条件变量 `condition_variable.wait` 的用法与问题解析 条件变量(`condition_variable`)是多线程编程中常用的同步机制之一,用于线程间的通信等待通知。`wait` 方法允许一个线程在某个条件未满足时进入阻塞状态,并在条件满足时被唤醒[^2]。 #### 1. 基本用法 `condition_variable.wait` 的基本用法需要与互斥锁(`mutex`)配合使用。调用 `wait` 时,当前线程会释放互斥锁并进入等待状态,直到另一个线程通过 `notify_one` 或 `notify_all` 唤醒它[^4]。 以下是一个简单的代码示例: ```cpp #include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool ready = false; void worker() { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return ready; }); // 等待条件为真 std::cout << "Worker thread is processing..." << std::endl; } int main() { std::thread t(worker); { std::lock_guard<std::mutex> lock(mtx); ready = true; // 修改条件 } cv.notify_one(); // 唤醒等待的线程 t.join(); return 0; } ``` #### 2. 常见问题 - **虚假唤醒**:即使没有调用 `notify_one` 或 `notify_all`,线程也可能从 `wait` 中返回。因此,推荐在 `wait` 中使用谓词(predicate)来确保条件真正满足[^3]。 - **死锁风险**:如果 `wait` 被调用时没有正确持有互斥锁,可能会导致死锁或未定义行为。 - **性能问题**:频繁调用 `notify_all` 可能导致不必要的上下文切换,影响性能[^2]。 #### 3. RAII 封装 为了简化条件变量的使用,可以采用 RAII(Resource Acquisition Is Initialization)模式封装互斥锁条件变量。例如,基于 C++ 的封装如下: ```cpp class CountDownLatch { public: explicit CountDownLatch(int count) : count_(count), mutex_(), condition_(&mutex_) {} void wait() { std::unique_lock<std::mutex> lock(mutex_); while (count_ > 0) { condition_.wait(lock); // 使用谓词避免虚假唤醒 } } void countDown() { std::unique_lock<std::mutex> lock(mutex_); if (--count_ == 0) { condition_.notifyAll(); // 当计数为零时广播 } } private: int count_; std::mutex mutex_; std::condition_variable condition_; }; ``` #### 4. Go 语言中的对比 在 Go 语言中,虽然没有直接的 `condition_variable`,但可以通过 `sync.Cond` 实现类似功能。以下是一个简单的 Go 示例[^5]: ```go package main import ( "fmt" "sync" ) var cond = sync.NewCond(&sync.Mutex{}) var counter = 0 func worker(id int) { cond.L.Lock() for counter != id { cond.Wait() // 等待条件满足 } fmt.Printf("Worker %d is processing...\n", id) counter++ cond.L.Unlock() cond.Signal() // 唤醒下一个线程 } func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { defer wg.Done() worker(i) }(i) } cond.L.Lock() cond.Signal() // 启动第一个线程 cond.L.Unlock() wg.Wait() } ``` ###
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值