C++并发编程之玩转condition_variable

2023-09-02 10:38:17 浏览数 (1)

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;
}

以上便是本次小项目的一些点,比较有意思,也非常的实用,在面试与实际学习的过程中会学到不少东西,欢迎大家与我一起探讨并发编程~

0 人点赞