C++ 并发编程指南(9)条件变量


前言

在并发编程中,线程同步是确保共享资源安全访问的关键。C++中的条件变量作为一种同步原语,允许线程在特定条件下等待或通知其他线程,从而协调线程的执行。本文将简要介绍C++条件变量的基本概念、使用方法和注意事项,并通过示例展示其在并发编程中的应用。通过学习本文,您将能够掌握C++条件变量的基本用法,提升程序的性能和可靠性。

一、条件变量

1、基本概念

条件变量是一个同步原语,它允许线程在一定条件下等待或通知其他线程。当某个条件不满足时,一个线程可以选择等待,而不是忙等待(即不断地检查条件是否满足)。当条件满足时,另一个线程会通知等待的线程,使其能够继续执行。

2、C++中的条件变量

在C++中,条件变量是通过<condition_variable>头文件提供的std::condition_variable类实现的。这个类提供了两个主要的成员函数:wait()notify_one()(或notify_all())。

  • wait(): 使当前线程进入等待状态,直到另一个线程调用notify_one()notify_all()。在等待期间,当前线程会释放它持有的所有锁,从而允许其他线程访问共享资源。当收到通知并重新获得锁后,线程会退出等待状态并继续执行。
  • notify_one(): 唤醒一个正在等待的线程。如果有多个线程在等待,则只有一个线程会被唤醒。
  • notify_all(): 唤醒所有正在等待的线程。

3、使用条件变量的基本步骤

  • 初始化条件变量和互斥锁:在使用条件变量之前,需要先创建一个std::condition_variable对象和一个std::mutex对象。互斥锁用于保护共享资源,防止多个线程同时访问。
  • 等待条件满足:在需要等待的线程中,首先锁定互斥锁,然后调用条件变量的wait()函数。这将释放互斥锁并使线程进入等待状态。
  • 修改条件并通知:当条件发生变化时(通常是由另一个线程修改共享资源导致的),拥有互斥锁的线程应该调用notify_one()notify_all()来唤醒等待的线程。
  • 继续执行:被唤醒的线程在重新获得互斥锁后会退出等待状态,并继续执行。

4、注意事项

  • 避免虚假唤醒:条件变量可能会导致虚假唤醒,即在没有调用notify_one()notify_all()的情况下,线程被唤醒。因此,在wait()返回后,应重新检查条件是否真正满足。
while (!(xxx条件) )
{
    //虚假唤醒发生,由于while循环,再次检查条件是否满足,
    //否则继续等待,解决虚假唤醒
    wait();  
}
  • 正确管理锁:条件变量的wait()函数需要一个std::unique_lock对象作为参数。std::unique_lock是一个更加灵活的锁对象,可以在不同的作用域内自动管理锁的获取和释放。
  • 正确处理异常:在锁定互斥锁和调用条件变量的过程中,如果发生异常,可能会导致锁没有被正确释放。因此,建议使用RAII(资源获取即初始化)技术来管理锁的生命周期,确保在异常发生时也能正确释放锁。

5、问题剖析

5.1、为什么只能使用std::unique_lock管理锁?

使用std::unique_lock而不是std::lock_guard来管理与条件变量配合使用的互斥锁的原因主要有以下几点:

  • 解锁再休眠:当线程调用wait()函数时,std::unique_lock会自动释放互斥锁,让其他线程有机会获取锁并修改共享资源。然后当前线程进入休眠状态,直到收到通知或满足条件后再次自动获取锁。
  • 灵活性和控制std::unique_lock提供了更多的灵活性,包括延迟锁定、可重入锁定以及支持条件变量等待。它还允许在等待过程中使用timeouttry_lock等高级功能,这些是std::lock_guard所不具备的。

总的来说,由于std::unique_lock具有在等待条件变量时释放锁的能力,并且在重新唤醒时能自动恢复锁定状态,它成为了与条件变量配合使用时的首选工具。而std::lock_guard虽然简单易用,但由于其设计初衷是用于保护简单的代码块,并在异常安全方面提供保障,因此在需要更复杂同步逻辑的条件变量场景中不太适用。

5.2、notify_one()与notify_all()的主要区别?

notify_one()notify_all()都是C++条件变量中用于唤醒等待的线程的方法,但它们在唤醒方式和使用场景上存在差异。具体分析如下:

  • 唤醒方式notify_one()只唤醒等待队列中的一个线程,而notify_all()会唤醒所有等待在条件变量上的线程。
  • 使用场景:如果多个线程在等待同一个条件,且只要一个线程处理就可以满足条件,那么notify_one()更为合适。它可以减少线程之间的竞争,提高程序的效率。相反,当条件发生变化,需要所有等待的线程都知晓并进行处理时,应该使用notify_all()。这通常用于状态变化需要通知所有线程的场景。

总的来说,选择notify_one()还是notify_all()取决于具体的同步需求和性能考虑。在不需要所有线程都被唤醒的情况下,使用notify_one()可以提高效率,而在需要广播状态变化时,使用notify_all()确保所有线程都能响应条件的变化。

6、使用条件变量的生产者-消费者模型示例

下面是一个使用C++条件变量的生产者-消费者模型示例:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool finished = false;

void producer(int id) {
    for (int i = 0; i < 5; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        data_queue.push(i);
        std::cout << "Producer " << id << " produced " << i << std::endl;
        lock.unlock();
        cv.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    finished = true;
    cv.notify_one();
}

void consumer() {
    while (!finished) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{return !data_queue.empty() || finished;});
        if (!data_queue.empty()) {
            int data = data_queue.front();
            data_queue.pop();
            std::cout << "Consumer consumed " << data << std::endl;
        }
    }
}

int main() {
    std::thread producer1(producer, 1);
    std::thread producer2(producer, 2);
    std::thread consumer1(consumer);
    producer1.join();
    producer2.join();
    consumer1.join();
    return 0;
}

在这个示例中,我们定义了两个生产者线程和一个消费者线程。生产者线程负责生产数据,并将数据放入队列中;消费者线程负责从队列中取出数据并消费。通过使用条件变量,我们可以实现生产者和消费者之间的同步关系。当队列为空时,消费者线程会等待生产者线程生产数据;当队列中有数据时,消费者线程会被唤醒并消费数据。

7、总结

C++中的条件变量是实现线程同步的强大工具。通过合理使用条件变量和互斥锁,我们可以有效地控制多个线程对共享资源的访问,确保数据的一致性和正确性。然而,在使用条件变量时,我们也需要注意避免虚假唤醒、死锁和异常处理等问题。

  • 27
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++11 标准提供了一些并发编程指南,帮助程序员处理并发编程中的常见问题。这些指南可以帮助开发者实现高效、可靠的多线程应用。 首先,C++11 引入了 std::thread 类,它是一种线程的表示方式,可以方便地创建和管理线程。通过 std::thread,我们可以启动一个新线程并指定要执行的函数或函数对象。此外,std::thread 还提供了一系列的成员函数,如 join() 和 detach(),用于等待线程结束或分离线程。 其次,C++11 还引入了 std::mutex 和 std::lock_guard 类,用于解决多线程下的竞争条件问题。std::mutex 是一种互斥量,可以通过调用 lock() 和 unlock() 来控制对共享资源的访问。std::lock_guard 是一种锁保护类型,它的构造函数会自动在当前作用域上锁,析构函数会自动解锁,确保锁的正确使用。 此外,C++11 提供了 std::condition_variable 类,用于实现多线程间的条件变量通信。std::condition_variable 允许线程等待某个条件的发生,并在条件满足时由其他线程进行通知。 还有一个重要的概念是原子操作,C++11 提供了 std::atomic 类模板来实现无锁编程。通过 std::atomic,我们可以对共享变量进行原子操作,避免了需要锁保护的临界区域。 最后,C++11 还引入了 std::future 类模板和 std::promise 类模板,用于实现异步计算和线程间的数据传递。std::future 可以保存一个异步操作(如函数调用)的结果,而 std::promise 则可以在某个时间点设置这个结果。 综上所述,C++11 并发指南中的一些关键特性包括 std::thread、std::mutex、std::lock_guard、std::condition_variable、std::atomic、std::future 和 std::promise。它们为我们提供了一些基本工具和机制,帮助我们更加方便地编写多线程应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值