c++多线程(6)------ 条件变量

  • 操作系统:ubuntu22.04
  • IDE:Visual Studio Code
  • 编程语言:C++11

条件变量(Condition Variable) 是实现线程间高效等待与通知机制的核心工具,通常与互斥锁配合使用,用于解决"生产者-消费者"、"任务队列"、"线程协调"等经典并发问题。

一、为什么需要条件变量?

❌ 问题:忙等待(Busy Waiting)效率低

cpp 复制代码
std::mutex mtx;
bool ready = false;

// 线程A(等待者)
while (!ready)
{
    // 空循环,持续检查
}
// do work...

// 线程B(通知者)
{
    std::lock_guard<std::mutex> lock(mtx);
    ready = true;
}
  • CPU 资源浪费:等待线程不断轮询,占用 CPU。
  • 响应延迟:无法立即响应状态变化。

✅ 解决方案:条件变量

  • 等待线程挂起(阻塞),不消耗 CPU。
  • 通知线程唤醒等待线程。
  • 高效、节能、响应及时。

二、C++11 中的条件变量类型

C++11 在 <condition_variable> 中提供了两种条件变量:

类型 说明
std::condition_variable 仅支持 std::unique_lockstd::mutex,性能更高,最常用
std::condition_variable_any 支持任意满足 BasicLockable 的锁(如 shared_mutex),但性能略低

✅ 推荐优先使用 std::condition_variable。

三、基本用法:wait() 与 notify_one() / notify_all()

核心 API:

cpp 复制代码
void wait(std::unique_lock<std::mutex>& lock);
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
void notify_one();  // 唤醒一个等待线程
void notify_all();  // 唤醒所有等待线程

🌰 示例:简单线程同步

cpp 复制代码
#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; }); // 等待 ready == true
    std::cout << "Worker: working now!\n";
}

void starter() 
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one(); // 通知一个等待线程
}

int main() 
{
    std::thread t1(worker);
    std::thread t2(starter);
    t1.join(); t2.join();
}

输出:

bash 复制代码
Worker: working now!

四、深入理解 wait() 的工作机制

cv.wait(lock, pred) 等价于:

cpp 复制代码
while (!pred()) 
{
    cv.wait(lock); // 内部:1. unlock mutex; 2. 阻塞等待; 3. 被唤醒后重新 lock
}

关键点:

  • 自动释放锁:调用 wait() 时,会自动释放传入的 unique_lock,允许其他线程获取锁并修改共享状态。
  • 原子性:释放锁 + 进入等待 是原子操作,避免通知丢失(lost wake-up)。
  • 虚假唤醒(Spurious Wakeup):即使没有调用 notify,线程也可能被唤醒(POSIX 允许)。因此必须使用谓词(predicate) 检查条件。

✅ 所以永远不要写 cv.wait(lock); 而不带谓词!

五、经典应用:生产者-消费者模型(任务队列)

cpp 复制代码
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>

std::queue< int > task_queue;
std::mutex mtx;
std::condition_variable cv;
bool done = false;

// 生产者
void producer( int id )
{
    for ( int i = 0; i < 5; ++i )
    {
        {
            std::lock_guard< std::mutex > lock( mtx );
            task_queue.push( id * 10 + i );
            std::cout << "Producer " << id << " produced " << id * 10 + i << "\n";
        }
        cv.notify_one();  // 通知消费者
        std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
    }
}

// 消费者
void consumer( int id )
{
    while ( true )
    {
        std::unique_lock< std::mutex > lock( mtx );
        cv.wait( lock, [] { return !task_queue.empty() || done; } );

        if ( done && task_queue.empty() )
            break;

        int task = task_queue.front();
        task_queue.pop();
        lock.unlock();  // 提前释放锁,减少临界区

        std::cout << "Consumer " << id << " processed " << task << "\n";
        std::this_thread::sleep_for( std::chrono::milliseconds( 150 ) );
    }
}

int main()
{
    std::thread p1( producer, 1 );
    std::thread p2( producer, 2 );
    std::thread c1( consumer, 1 );
    std::thread c2( consumer, 2 );

    p1.join();
    p2.join();
    done = true;
    cv.notify_all();  // 通知所有消费者退出
    c1.join();
    c2.join();
}

输出:

bash 复制代码
Producer 1 produced 10
Consumer 2 processed 10Producer 
2 produced 20
Consumer 1 processed 20
Producer 1 produced 11
Producer 2 produced 21
Consumer 2 processed 11
Consumer 1 processed 21
Producer 1 produced 12
Producer 2 produced 22
Consumer 2 processed 12
Consumer 1 processed 22
Producer 1 produced 13
Producer 2 produced 23
Producer 1 produced 14
Producer 2 produced 24
Consumer 2 processed 13
Consumer 1 processed 23
Consumer 1 processed 14
Consumer 2 processed 24

关键设计:

  • 使用 done 标志优雅退出。
  • cv.notify_one():一个任务唤醒一个消费者(避免惊群)。
  • 消费后提前解锁,提高并发性。

六、带超时的等待:wait_for() 与 wait_until()

适用于"最多等待 N 秒"的场景。

cpp 复制代码
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, std::chrono::seconds(5), []{ return ready; })) 
{
    std::cout << "Condition met!\n";
}
else 
{
    std::cout << "Timeout! Condition not met.\n";
}
  • wait_for:相对时间(duration)
  • wait_until:绝对时间(time_point)

✅ 返回值:若因条件满足而唤醒,返回 true;若超时,返回 false。

七、常见错误与最佳实践

❌ 错误1:忘记使用谓词(导致虚假唤醒崩溃)

cpp 复制代码
// 危险!可能虚假唤醒后继续执行
cv.wait(lock);
// 此时 ready 可能仍为 false!

✅ 正确:

cpp 复制代码
cv.wait(lock, []{ return ready; });

❌ 错误2:在未加锁时修改条件并通知

cpp 复制代码
ready = true;           // ❌ 未加锁!
cv.notify_one();        // 可能通知丢失

✅ 正确:

cpp 复制代码
{
    std::lock_guard<std::mutex> lock(mtx);
    ready = true;
}
cv.notify_one(); // 通知可在锁外,但修改必须在锁内

📝 通知可以在锁外调用(C++ 允许),但修改共享状态必须在锁内。

❌ 错误3:使用 std::lock_guard 与 wait()

cpp 复制代码
std::lock_guard<std::mutex> lock(mtx);
cv.wait(lock, ...); // ❌ 编译错误!wait 需要 unique_lock

✅ 正确:

cpp 复制代码
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, ...);

❌ 错误4:滥用 notify_all()

  • 若只有一个线程能处理任务,用 notify_one() 更高效。
  • notify_all() 适用于广播场景(如所有线程需响应"退出"信号)。

八、notify_one() vs notify_all()

场景 推荐
任务队列(一个任务 → 一个消费者) notify_one()
状态变更需所有线程响应(如 shutdown) notify_all()
不确定有多少线程在等 notify_all()(安全但低效)

九、总结:条件变量使用模板

cpp 复制代码
// 共享状态
bool condition = false;
std::mutex mtx;
std::condition_variable cv;

// 等待线程
{
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return condition; });
    // 条件满足,处理逻辑
}

// 通知线程
{
    std::lock_guard<std::mutex> lock(mtx);
    condition = true;
}
cv.notify_one(); // 或 notify_all()

最佳实践清单:

  • 总是使用谓词版本的 wait()。
  • 修改共享状态必须在互斥锁保护下。
  • 使用 std::unique_lock(不是 lock_guard)。
  • 优先用 notify_one(),除非需要广播。
  • 考虑超时机制(wait_for)避免永久阻塞。
  • 避免在持有锁时做耗时操作(提前 unlock)。
相关推荐
wuk9989 分钟前
基于有限差分法的二维平面热传导模型MATLAB实现
开发语言·matlab·平面
杨筱毅2 小时前
【C++】【常见面试题】最简版带大小和超时限制的LRU缓存实现
c++·面试
初见无风2 小时前
2.5 Lua代码中string类型常用API
开发语言·lua·lua5.4
做运维的阿瑞2 小时前
用 Python 构建稳健的数据分析流水线
开发语言·python·数据分析
左师佑图2 小时前
综合案例:Python 数据处理——从Excel文件到数据分析
开发语言·python·数据分析·excel·pandas
陌路203 小时前
C23构造函数与析构函数
开发语言·c++
_OP_CHEN3 小时前
C++进阶:(二)多态的深度解析
开发语言·c++·多态·抽象类·虚函数·多态的底层原理·多态面试题
CsharpDev-奶豆哥3 小时前
JavaScript性能优化实战大纲
开发语言·javascript·性能优化
小妖同学学AI4 小时前
Rust 深度解析:变量、可变性与所有权的“安全边界”
开发语言·安全·rust
2301_764441334 小时前
基于python构建的低温胁迫实验
开发语言·python