0

0

c++如何使用条件变量_c++ condition_variable线程同步详解

尼克

尼克

发布时间:2025-09-21 14:52:01

|

422人浏览过

|

来源于php中文网

原创

std::condition_variable通过“等待-通知”机制解决线程间协作问题,避免忙等。它需与std::mutex和std::unique_lock配合使用,wait()在条件不满足时释放锁并休眠,notify_one()/notify_all()唤醒等待线程,常用于生产者-消费者模型、线程池调度等场景,核心是避免虚假唤醒和丢失通知,确保高效安全的线程同步。

c++如何使用条件变量_c++ condition_variable线程同步详解

在C++多线程编程中,

std::condition_variable
是实现线程间同步和通信的关键工具,它允许一个或多个线程等待某个特定条件成立,而不会像忙等(busy-waiting)那样持续消耗CPU资源。简单来说,它提供了一种高效的“等待-通知”机制,让线程在条件不满足时休眠,在条件满足时被唤醒。

解决方案

要使用

std::condition_variable
进行线程同步,你通常需要配合
std::mutex
std::unique_lock<std::mutex>
。核心思想是:当一个线程需要等待某个条件时,它会获取一个互斥锁,然后调用条件变量的
wait()
方法。
wait()
方法会原子性地释放互斥锁并使线程进入休眠状态。当另一个线程改变了条件并希望唤醒等待的线程时,它也会获取互斥锁,修改条件,然后调用条件变量的
notify_one()
notify_all()
方法。被唤醒的线程会重新获取互斥锁,并检查条件是否真的满足(因为可能存在虚假唤醒),如果满足则继续执行,否则再次等待。

下面是一个经典的生产者-消费者模型示例,它清晰地展示了

std::condition_variable
的使用:

#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>

// 共享资源
std::mutex mtx; // 保护共享数据
std::condition_variable cv; // 条件变量
std::queue<int> data_queue; // 共享数据队列
bool stop_producing = false; // 停止生产的标志

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时
        {
            std::unique_lock<std::mutex> lock(mtx); // 获取锁
            data_queue.push(i); // 生产数据
            std::cout << "Producer pushed: " << i << std::endl;
            cv.notify_one(); // 通知一个等待的消费者
        } // 锁在这里自动释放
    }

    // 生产完毕,通知所有消费者可以停止等待了
    {
        std::unique_lock<std::mutex> lock(mtx);
        stop_producing = true; // 设置停止标志
        std::cout << "Producer finished production, notifying all consumers." << std::endl;
    } // 锁在这里自动释放
    cv.notify_all(); // 唤醒所有等待的消费者
}

void consumer(int id) {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx); // 获取锁
        // 等待条件:队列不为空 或者 生产者已停止
        // wait()函数会自动释放锁并休眠,被唤醒时会重新获取锁
        cv.wait(lock, [&]{ return !data_queue.empty() || stop_producing; });

        // 如果队列为空且生产者已停止,说明没有更多数据了,消费者可以退出了
        if (data_queue.empty() && stop_producing) {
            std::cout << "Consumer " << id << " finished." << std::endl;
            break;
        }

        // 处理数据
        int data = data_queue.front();
        data_queue.pop();
        std::cout << "Consumer " << id << " consumed: " << data << std::endl;
    }
}

int main() {
    std::thread prod_thread(producer);
    std::thread cons_thread1(consumer, 1);
    std::thread cons_thread2(consumer, 2); // 多个消费者

    prod_thread.join();
    cons_thread1.join();
    cons_thread2.join();

    std::cout << "All threads finished." << std::endl;
    return 0;
}

这段代码里,生产者线程在每次生产完数据后,会通过

cv.notify_one()
唤醒一个消费者线程。当所有数据生产完毕,它会设置
stop_producing
标志,并通过
cv.notify_all()
唤醒所有消费者,告诉它们“活儿干完了,可以收工了”。消费者线程则在
cv.wait()
中等待,直到队列中有数据或者生产者发出停止信号。这个
wait
的第二个参数,也就是lambda表达式,是一个谓词(predicate),它会在
wait
返回前被检查,这能有效避免虚假唤醒带来的问题。

立即学习C++免费学习笔记(深入)”;

为什么我们需要
std::condition_variable
?它解决了哪些并发难题?

说实话,在多线程编程里,光有互斥锁(

std::mutex
)是远远不够的。互斥锁只能保证同一时间只有一个线程访问共享资源,避免数据竞争。但很多时候,线程之间不仅仅是“互斥”的关系,它们还需要“协作”。比如,一个线程需要等待另一个线程完成某个任务,或者等待某个条件满足才能继续执行。

如果没有

std::condition_variable
,我们可能会怎么做?最直观的,可能就是忙等(busy-waiting)了。一个线程会不断地去检查某个共享变量,比如:

// 糟糕的忙等示例
bool data_ready = false;
void consumer_bad() {
    while (!data_ready) {
        // 什么也不做,或者短暂休眠
        std::this_thread::sleep_for(std::chrono::microseconds(1)); // 稍微好一点,但仍然是忙等
    }
    // 处理数据
}

这种方式的弊端非常明显:它会白白消耗大量的CPU周期,即使条件不满足,线程也一直在运行,浪费资源。在实际项目中,这简直是性能杀手。

std::condition_variable
正是为了解决这种“等待某个条件”的协作问题而生的。它让等待的线程可以高效地进入休眠状态,释放CPU资源,直到被明确地通知才会被唤醒。它主要解决了以下几类并发难题:

  1. 生产者-消费者问题: 这是最经典的场景。生产者生产数据,消费者消费数据。如果队列空了,消费者就得等;如果队列满了(在有界队列中),生产者就得等。条件变量完美地协调了这种等待。
  2. 线程池任务调度: 线程池中的工作线程需要等待任务队列中有新的任务到来。如果没有任务,它们就休眠;有新任务了,就被唤醒去执行。
  3. 一次性事件通知: 比如一个主线程启动了多个子线程去执行任务,然后主线程需要等待所有子线程都完成初始化或者某个特定阶段后才能继续。
  4. 资源可用性等待: 线程需要等待某个共享资源变得可用,例如文件句柄、网络连接等。
  5. 优雅的线程终止: 就像上面示例中,通过一个标志位和
    notify_all
    ,可以通知所有等待的线程安全地退出。

在我看来,

std::condition_variable
是构建高效、响应式并发程序的基石之一。它把“等待”从低效的轮询检查,变成了高效率的事件驱动,这在现代多核系统中尤其重要。

PathFinder
PathFinder

AI驱动的销售漏斗分析工具

下载

std::condition_variable
的核心机制:
wait()
notify()
的工作原理

理解

wait()
notify()
的工作原理,是正确使用
std::condition_variable
的关键。它们并不是简单地让线程睡着或醒来,背后有一套精妙的原子操作。

wait()
的工作原理:

当一个线程调用

cv.wait(lock, predicate)
时,它的内部流程大致是这样的:

  1. 检查谓词: 首先,
    wait()
    会检查你提供的谓词(lambda表达式)。如果谓词返回
    true
    ,说明条件已经满足,线程就不需要等待,直接返回,并保持锁的持有状态。
  2. 原子性释放锁并休眠: 如果谓词返回
    false
    (或者你没有提供谓词,直接调用
    cv.wait(lock)
    ),
    wait()
    原子性地执行两个操作:
    • 释放
      lock
      std::unique_lock
      对象持有的互斥锁)。
    • 将当前线程放入条件变量的等待队列中,并使其进入休眠状态(阻塞)。 这个原子性非常重要,它确保了在锁被释放和线程进入休眠之间,不会有其他线程在没有获取锁的情况下修改条件变量,从而避免了丢失通知(lost wakeup)的风险。
  3. 被唤醒并重新获取锁: 当其他线程调用
    notify_one()
    notify_all()
    时,等待队列中的线程会被唤醒。被唤醒的线程会尝试重新获取之前释放的互斥锁。
  4. 再次检查谓词: 成功获取锁后,
    wait()
    会再次检查谓词。
    • 如果谓词返回
      true
      ,线程就从
      wait()
      调用中返回,继续执行后续代码。
    • 如果谓词返回
      false
      ,线程会再次释放锁,并重新进入休眠状态。 这个重复检查谓词的机制,正是为了处理虚假唤醒(spurious wakeups)。虚假唤醒是指线程在没有收到
      notify
      信号的情况下,或者在条件尚未满足时,被操作系统调度器错误地唤醒。虽然不常见,但标准允许这种情况发生,所以我们必须用一个
      while
      循环(或者
      wait
      的谓词参数)来包裹条件检查,确保只有在条件真正满足时才继续执行。

notify_one()
notify_all()
的工作原理:

当一个线程调用

notify_one()
notify_all()
时,它的内部流程相对简单:

  1. notify_one()
    • 从条件变量的等待队列中选择一个(通常是第一个)等待的线程。
    • 唤醒这个线程,使其从休眠状态变为可运行状态。被唤醒的线程会去尝试重新获取互斥锁。
    • 选择哪个线程是未定义的行为,你不能依赖特定的顺序。
  2. notify_all()
    • 唤醒条件变量等待队列中的所有等待线程。
    • 所有被唤醒的线程都会尝试重新获取互斥锁。

何时使用

notify_one()
,何时使用
notify_all()

  • notify_one()
    当你确切知道只有一个线程需要处理这个条件时,或者有多个消费者,但每次只生产一个物品,只唤醒一个消费者就足够了,避免不必要的线程上下文切换。例如,上面生产者-消费者模型中,每生产一个数据,就
    notify_one()
  • notify_all()
    当多个线程可能需要响应同一个条件时,或者你无法确定哪个线程需要被唤醒时。例如,当一个全局状态改变,所有等待这个状态的线程都需要重新评估时;或者在线程池中,当有多个任务被加入队列,但你不知道哪些工作线程空闲时;以及在上面示例中,生产者停止生产时,需要通知所有消费者检查
    stop_producing
    标志。
    notify_all()
    在关闭(shutdown)场景下也特别有用,可以确保所有等待的线程都能检查到退出条件并优雅退出。

选择正确的通知方式,既能保证程序的正确性,也能在一定程度上影响性能。

使用
condition_variable
时常见的陷阱与最佳实践

虽然

std::condition_variable
功能强大,但它也是一个容易出错的同步原语。一些常见的陷阱如果没注意到,轻则程序行为异常,重则死锁或数据损坏。

常见的陷阱:

  1. 没有使用互斥锁保护共享条件: 这是最基础也是最致命的错误。条件变量本身不保护共享数据。你必须使用
    std::mutex
    来保护所有被条件变量依赖的共享数据(例如示例中的
    data_queue
    stop_producing
    )。如果没有锁,多个线程同时修改条件,会导致数据竞争,程序行为不可预测。
  2. 忘记了
    wait()
    的谓词或
    while
    循环:
    就像前面提到的,
    wait()
    可能会发生虚假唤醒。如果你只是简单地
    if (!condition) cv.wait(lock);
    ,那么在虚假唤醒后,线程会错误地认为条件已满足并继续执行,导致逻辑错误。始终使用
    cv.wait(lock, [&]{ return condition; });
    或者
    while (!condition) { cv.wait(lock); }
  3. 在不持有锁的情况下修改条件或调用
    notify()
    修改共享条件必须在持有互斥锁的情况下进行。如果你在没有锁的情况下修改了条件,然后调用
    notify()
    ,那么一个等待的线程可能在条件被修改和
    notify()
    之间进入等待状态,从而错过通知,导致永久休眠(lost wakeup)。
    notify()
    本身可以在不持有锁的情况下调用(虽然通常推荐在持有锁时调用,因为这样可以确保条件在通知时是稳定的),但修改条件变量所依赖的共享状态必须在锁的保护下。
  4. notify()
    时机不当:
    有时,开发者会先释放锁,然后才调用
    notify()
    。这通常是没问题的,甚至在某些高性能场景下,可以减少被唤醒线程重新获取锁的竞争。但如果你的逻辑要求被唤醒的线程能够立即获取锁并处理数据,那么在持有锁的时候调用
    notify()
    可能更直接。关键在于理解你的程序流和竞争条件。
  5. 死锁: 这是一个普遍的并发问题,与条件变量结合时也可能出现。例如,如果一个线程持有锁A,然后尝试等待条件变量(这会释放锁A),但另一个线程需要锁A才能修改条件并发出通知,这就可能导致死锁。注意锁

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

847

2023.08.22

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

107

2023.09.25

lambda表达式
lambda表达式

Lambda表达式是一种匿名函数的简洁表示方式,它可以在需要函数作为参数的地方使用,并提供了一种更简洁、更灵活的编码方式,其语法为“lambda 参数列表: 表达式”,参数列表是函数的参数,可以包含一个或多个参数,用逗号分隔,表达式是函数的执行体,用于定义函数的具体操作。本专题为大家提供lambda表达式相关的文章、下载、课程内容,供大家免费下载体验。

215

2023.09.15

python lambda函数
python lambda函数

本专题整合了python lambda函数用法详解,阅读专题下面的文章了解更多详细内容。

192

2025.11.08

Python lambda详解
Python lambda详解

本专题整合了Python lambda函数相关教程,阅读下面的文章了解更多详细内容。

61

2026.01.05

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

765

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

377

2025.12.24

java多线程相关教程合集
java多线程相关教程合集

本专题整合了java多线程相关教程,阅读专题下面的文章了解更多详细内容。

32

2026.01.21

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

37

2026.03.12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
C# 教程
C# 教程

共94课时 | 11.2万人学习

C 教程
C 教程

共75课时 | 5.4万人学习

C++教程
C++教程

共115课时 | 21.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号