C 并发编程之玩转condition_variable
0.导语
最近在看并发编程相关的代码,自己顺手从0开始写了个小项目玩转并发场景下的生产消费者模型,如果你想提高多线程编程方面的能力,想熟练掌握condition_variable的使用,甚至想在面试当中凸显这一块的技术时,不妨与我一起探讨本篇文章。
本篇文章将会从会引入以下几个版本:
1.CPU轮询等待版单生产者单消费者:
该版本使用了简单的轮询机制,生产者不断地检查消费者是否已经消费完数据。这种模式简单直接,但效率较低,因为生产者在没有数据时仍然在忙等待。
2.等待通知版单生产者单消费者:
该版本引入了等待通知机制,生产者在没有数据时会等待消费者的通知。这种模式避免了忙等待,提高了效率,同时减少了资源消耗。
3.等待通知版单生产者多消费者:
在这个版本中,引入了多个消费者,它们共享生产者的数据。生产者在产生数据后,通知所有消费者进行处理。
4.等待通知版多生产者多消费者:
这个版本支持多个生产者和多个消费者,生产者之间和消费者之间共享数据。生产者在产生数据后,通知所有消费者进行处理。
5.单生产者多消费者并行版:
在这个版本中,引入了并行处理机制,多个消费者可以同时处理数据。生产者产生数据后,多个消费者并行处理,提高了整体处理速度。
6.多生产者多消费者并行版:
这个版本支持多个生产者和多个消费者,并且允许并行处理。多个生产者并行产生数据,多个消费者并行处理数据,提高了整体并发能力。
7.支持Lambda回调的优雅停止版:
在这个版本中,引入了Lambda回调函数,用于优雅地停止并发处理。可以通过调用回调函数来停止生产者和消费者的处理,并进行清理工作
通过这几个版本的学习可以掌握:
1.多线程和并发编程:通过这些版本的描述,您可以了解到如何在C 中使用多线程和并发编程来处理并行任务。您将学习如何创建线程、控制线程的执行、线程间的数据共享和同步等。
2.同步机制:在这些版本中,介绍了不同的同步机制,如轮询等待、等待通知和回调函数。您将学习如何使用互斥锁、条件变量、信号量等来实现线程间的同步和协调。
3.Lambda表达式:在支持Lambda回调的版本中,您将学习如何使用C 11引入的Lambda表达式来编写简洁而灵活的回调函数,以实现优雅的停止机制。
4.代码组织和构建工具:提到了支持bazel编译的能力,这可以让您学习如何使用构建工具来组织和管理复杂的C 项目。
5.condition_variable、mutex、unique_lock
1. CPU轮训等待版单生产者单消费者
这个版本效率非常低,而低效率来自于繁忙等待循环,因为CPU停留在循环中什么都不做。忙碌等待并不是最佳策略。根本原因是,除了轮询,我们没有办法让一个线程知道另一个线程已经完成。我们需要一个线程更直接的方式来通知其他线程。条件变量是为这些场景创建的。
代码语言:javascript复制for (;;) {
std::unique_lock<std::mutex> ul(mutex_);
data_ = rand() % 100;
std::cout << "produce data: " << data_ << std::endl;
ready_ = true;
ul.unlock();
while (ready_) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
2.等待通知版单生产者单消费者
在这个版本我们借助condition_variable,完成多线程之间的同步操作。
代码语言:javascript复制std::condition_variable cv_;
bool ready_{false};
生产者会不断生成一个随机数并将其存储在 data_ 变量中,然后将 ready_ 标志设置为 true,表示有可用的数据。接着,它通知等待的消费者线程,然后自己等待消费者线程处理完数据。
代码语言:javascript复制cv_.wait(ul, [this]() { return !ready_; });
消费者将会在一个无限循环中等待生产者通知数据的可用性。当 ready_ 标志为 true 时,它会从 data_ 变量中获取数据并进行处理,然后将 ready_ 标志设置为 false,表示数据已经被消费。接着,它通知生产者线程,然后自己等待生产者线程生成新的数据。
代码语言:javascript复制cv_.wait(ul, [this]() { return ready_; });
3.等待通知版单生产者多消费者
前面的版本中,我们还是单生产,单个消费者,如何做到多个消费者抢占消费?
此时需要引入队列,我们将任务丢到队列中去,随后多个消费者进行消费即可,与上述等待条件不同点在于队列的状态。
对于生产者:如果队列大小未达到 max_queue_size_ 的限制,如果队列已满,则生产者线程将等待消费者线程从队列中取走一些数据。
代码语言:javascript复制cv_.wait(ul, [this]() { return queue_.size() < max_queue_size_; });
对于消费者:队列有数据就消费,否则等待。
代码语言:javascript复制cv_.wait(ul, [this]() { return !queue_.empty(); });
4.等待通知版多生产者多消费者
对于这个版本比较简单,基于第三个版本继续优化,创建n个生产者线程即可。
5.单生产者多消费者并行版
对于以上版本有个比较大的问题,当生产者生产的数据到达上限时,消费者此时在消费,而生产者并没有动起来,它在等待消费者消费完才能进行,如何让生产者与消费者同时运转呢?
改进点在于使用多个cv,来回切换通知。
代码语言:javascript复制std::condition_variable cv_producer_;
std::condition_variable cv_consumer_;
6.多生产者多消费者并行版
基于5进行改造,支持多个生产者即可。
7.支持Lambda回调的优雅停止版
在上面版本中,我们的程序是一直生产、一直消费,如何优雅停止住?
这个停止条件能够让用户去控制,例如:我想写一个lambda回调函数,是否可以支持呢?
答案是可以的,例如:
代码语言:javascript复制w.SetStopConditionCallback([&]() {
return w.GetProducedCount() >= threshold;
});
这个例子为:当生产者生产的数量到达一定阈值时,程序结束。
因此,我们便可以写出这样的代码。
代码语言:javascript复制if (stop_condition_callback_() && queue_.empty()) {
break;
}
以上便是本次小项目的一些点,比较有意思,也非常的实用,在面试与实际学习的过程中会学到不少东西,欢迎大家与我一起探讨并发编程~