在 C++ 编程的世界里,并发编程是一个既强大又复杂的领域。当多个线程同时运行时,如何让它们高效且安全地协作就成了一个关键问题。条件变量就是 C++ 为我们提供的一个用于线程间同步的重要工具。接下来,咱们就一起深入探讨一下它的正确使用模式。

一、条件变量的基本概念

在并发编程中,我们常常会遇到这样的情况:一个线程需要等待某个条件满足后才能继续执行,而这个条件的满足可能由另一个线程来触发。条件变量就是为了解决这类问题而设计的。简单来说,条件变量允许一个线程在某个条件不满足时进入等待状态,直到其他线程通知它条件已经满足。

在 C++ 中,条件变量定义在 <condition_variable> 头文件中,主要有 std::condition_variablestd::condition_variable_any 两种类型。std::condition_variable 只能和 std::unique_lock<std::mutex> 一起使用,而 std::condition_variable_any 可以和任何满足互斥锁要求的锁类型一起使用。不过,通常情况下,我们使用 std::condition_variable 就足够了。

二、条件变量的基本使用示例

下面是一个简单的示例,展示了条件变量的基本使用方法:

#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 working..." << std::endl;
}

// 主线程函数
int main() {
    std::thread t(worker);

    {
        std::lock_guard<std::mutex> lock(mtx);
        // 设置条件为真
        ready = true;
    }
    // 通知等待的线程
    cv.notify_one();

    t.join();
    std::cout << "Main thread is done." << std::endl;
    return 0;
}

代码解释

  1. 定义互斥锁和条件变量:我们定义了一个 std::mutex 类型的互斥锁 mtx 和一个 std::condition_variable 类型的条件变量 cv,以及一个布尔变量 ready 来表示条件是否满足。
  2. 工作线程:在 worker 函数中,我们首先创建了一个 std::unique_lock 对象 lock,它会自动锁定互斥锁 mtx。然后调用 cv.wait(lock, []{ return ready; }) 来等待条件变量的通知。wait 函数会释放互斥锁并进入等待状态,直到其他线程调用 notify_onenotify_all 通知它,并且 ready 条件为真。
  3. 主线程:在 main 函数中,我们创建了一个工作线程 t。然后使用 std::lock_guard 锁定互斥锁,将 ready 设置为 true,表示条件已经满足。最后调用 cv.notify_one() 通知等待的线程。
  4. 等待线程结束:调用 t.join() 等待工作线程结束,然后输出主线程完成的信息。

三、条件变量的应用场景

生产者 - 消费者模型

生产者 - 消费者模型是并发编程中一个经典的应用场景。在这个模型中,生产者线程负责生产数据,消费者线程负责消费数据。当缓冲区满时,生产者线程需要等待;当缓冲区为空时,消费者线程需要等待。条件变量可以很好地解决这个问题。

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

std::mutex mtx;
std::condition_variable not_full, not_empty;
std::queue<int> buffer;
const int MAX_SIZE = 5;

// 生产者线程函数
void producer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待缓冲区不满
        not_full.wait(lock, []{ return buffer.size() < MAX_SIZE; });
        buffer.push(i);
        std::cout << "Produced: " << i << std::endl;
        // 通知消费者缓冲区不为空
        not_empty.notify_one();
    }
}

// 消费者线程函数
void consumer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待缓冲区不为空
        not_empty.wait(lock, []{ return!buffer.empty(); });
        int item = buffer.front();
        buffer.pop();
        std::cout << "Consumed: " << item << std::endl;
        // 通知生产者缓冲区不满
        not_full.notify_one();
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();

    std::cout << "All tasks are done." << std::endl;
    return 0;
}

代码解释

  1. 定义互斥锁和条件变量:我们定义了两个条件变量 not_fullnot_empty,分别用于通知生产者缓冲区不满和消费者缓冲区不为空。
  2. 生产者线程:在 producer 函数中,首先等待 not_full 条件变量的通知,确保缓冲区不满。然后将数据放入缓冲区,并通知消费者线程缓冲区不为空。
  3. 消费者线程:在 consumer 函数中,首先等待 not_empty 条件变量的通知,确保缓冲区不为空。然后从缓冲区取出数据,并通知生产者线程缓冲区不满。
  4. 主线程:创建生产者和消费者线程,并等待它们结束。

多线程任务同步

在多线程任务中,有时需要等待所有线程完成某个阶段的任务后才能继续执行下一个阶段的任务。条件变量可以用于实现这种同步。

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

std::mutex mtx;
std::condition_variable cv;
int ready_count = 0;
const int THREAD_COUNT = 5;

// 线程函数
void task() {
    // 模拟任务执行
    std::this_thread::sleep_for(std::chrono::seconds(1));

    {
        std::lock_guard<std::mutex> lock(mtx);
        ++ready_count;
    }
    // 通知主线程
    cv.notify_one();
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < THREAD_COUNT; ++i) {
        threads.emplace_back(task);
    }

    {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待所有线程完成任务
        cv.wait(lock, []{ return ready_count == THREAD_COUNT; });
    }

    std::cout << "All threads are ready." << std::endl;

    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

代码解释

  1. 定义互斥锁和条件变量:定义了一个互斥锁 mtx 和一个条件变量 cv,以及一个计数器 ready_count 用于记录已经完成任务的线程数量。
  2. 线程函数:在 task 函数中,模拟任务执行,然后将 ready_count 加 1,并通知主线程。
  3. 主线程:创建多个线程,并等待所有线程完成任务。调用 cv.wait(lock, []{ return ready_count == THREAD_COUNT; }) 等待所有线程完成任务。

四、条件变量的技术优缺点

优点

  1. 高效同步:条件变量可以让线程在等待条件满足时进入休眠状态,避免了忙等待,提高了系统的效率。
  2. 简化编程:使用条件变量可以简化线程间同步的代码,使代码更加清晰易懂。
  3. 灵活控制:可以使用 notify_one 通知一个等待的线程,也可以使用 notify_all 通知所有等待的线程。

缺点

  1. 使用复杂:条件变量的使用需要和互斥锁配合,使用不当容易导致死锁等问题。
  2. 虚假唤醒wait 函数可能会出现虚假唤醒的情况,即没有收到通知也会从等待状态返回。因此,在使用 wait 函数时,需要使用谓词来检查条件是否真的满足。

五、条件变量的注意事项

避免死锁

在使用条件变量时,一定要注意避免死锁。死锁通常是由于线程在持有锁的情况下等待条件变量的通知,而其他线程又需要获取这个锁才能通知它。为了避免死锁,应该在调用 wait 函数时释放互斥锁。

处理虚假唤醒

wait 函数可能会出现虚假唤醒的情况,因此在使用 wait 函数时,一定要使用谓词来检查条件是否真的满足。例如:

std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });

正确使用 notify_onenotify_all

notify_one 用于通知一个等待的线程,而 notify_all 用于通知所有等待的线程。在选择使用哪个函数时,需要根据具体的应用场景来决定。

六、文章总结

条件变量是 C++ 并发编程中一个非常重要的工具,它可以帮助我们实现线程间的高效同步。通过合理使用条件变量,我们可以解决很多并发编程中的问题,如生产者 - 消费者模型、多线程任务同步等。

在使用条件变量时,需要注意避免死锁、处理虚假唤醒等问题。同时,要根据具体的应用场景选择合适的通知方式(notify_onenotify_all)。

总之,掌握条件变量的正确使用模式可以让我们的并发编程更加高效、安全和可靠。