C++ 条件变量,互斥锁

C++ 中多线程编程的两个核心同步原语:互斥锁 (Mutex)条件变量 (Condition Variable)。它们是实现线程间安全通信和协调的关键。

1. 互斥锁 (Mutex)

核心概念

互斥锁用于保护共享数据 ,确保同一时间只有一个线程可以访问该数据,从而避免数据竞争 (Data Race)

原理 :线程在访问共享数据前先上锁 (lock) ,如果锁已被其他线程占用,则当前线程阻塞 (block) 等待。访问完成后解锁 (unlock),让其他线程有机会获取锁。

C++ 中的互斥锁 (<mutex> 头文件)

1. std::mutex

最基本的互斥锁。

cpp

复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex g_mutex; // 全局互斥锁
int shared_data = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        g_mutex.lock();    // 上锁
        ++shared_data;     // 临界区代码
        g_mutex.unlock();  // 解锁
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Final value: " << shared_data << std::endl; // 正确输出 200000
    return 0;
}
2. std::lock_guard (推荐使用)

RAII 风格的锁管理,在构造时自动上锁,析构时自动解锁,即使发生异常也能保证解锁,避免死锁。

cpp

复制代码
void safe_increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(g_mutex); // 构造时上锁,析构时解锁
        ++shared_data;
    } // lock_guard 在此析构,自动解锁
}
3. std::unique_lock (更灵活)

lock_guard 更灵活,可以手动控制上锁和解锁的时机,是条件变量必需的伙伴。

cpp

复制代码
void flexible_increment() {
    for (int i = 0; i < 100000; ++i) {
        std::unique_lock<std::mutex> lock(g_mutex); // 自动上锁
        ++shared_data;
        // 可以手动提前解锁,不需要等到作用域结束
        lock.unlock();
        // ... 这里可以执行一些不涉及共享数据的操作
    }
}

2. 条件变量 (Condition Variable)

核心概念

条件变量用于线程间的通信和协调 。它允许一个线程等待某个条件 成立,而其他线程在条件成立时通知等待的线程。

典型生产者-消费者模式

  • 消费者线程等待"缓冲区不为空"的条件

  • 生产者线程在放入数据后通知消费者

C++ 中的条件变量 (<condition_variable> 头文件)

基本使用模式

cpp

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

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
const int MAX_SIZE = 5;

// 生产者线程
void producer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待条件:队列未满
        // 使用lambda表达式作为等待条件
        cv.wait(lock, [] { 
            return data_queue.size() < MAX_SIZE; 
        });
        
        data_queue.push(i);
        std::cout << "Produced: " << i << std::endl;
        
        lock.unlock(); // 手动解锁(可选,notify之前解锁更好)
        cv.notify_one(); // 通知一个等待的消费者
    }
}

// 消费者线程
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待条件:队列不为空
        cv.wait(lock, [] { 
            return !data_queue.empty(); 
        });
        
        int data = data_queue.front();
        data_queue.pop();
        std::cout << "Consumed: " << data << std::endl;
        
        lock.unlock();
        cv.notify_one(); // 通知生产者可能有空位了
        
        if (data == 9) break; // 收到最后一个数据后退出
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons(consumer);
    
    prod.join();
    cons.join();
    
    return 0;
}

条件变量的关键方法

  1. wait(lock, predicate):

    • 原子地解锁并阻塞当前线程

    • 被唤醒后重新获取锁

    • 检查 predicate 条件,如果为 false 则继续等待

  2. notify_one():

    • 唤醒一个等待中的线程(如果有)
  3. notify_all():

    • 唤醒所有等待中的线程

3. 为什么条件变量需要互斥锁?

条件变量必须与互斥锁配合使用,原因如下:

  1. 原子性操作:检查条件和进入等待必须是原子操作,否则可能发生:

    • 线程A检查条件 → 条件不满足

    • 线程B修改条件并发出通知

    • 线程A才开始等待 → 通知丢失,线程A永远等待

  2. 保护共享状态:条件变量等待的"条件"通常是共享数据,需要用互斥锁保护。


4. 完整的生产者-消费者示例

cpp

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

class ThreadSafeQueue {
private:
    std::queue<int> queue_;
    std::mutex mtx_;
    std::condition_variable cv_producer_;
    std::condition_variable cv_consumer_;
    const int max_size_ = 5;
    bool stop_ = false;

public:
    void push(int value) {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_producer_.wait(lock, [this] { 
            return queue_.size() < max_size_ || stop_; 
        });
        
        if (stop_) return;
        
        queue_.push(value);
        std::cout << "Produced: " << value << std::endl;
        cv_consumer_.notify_one();
    }

    int pop() {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_consumer_.wait(lock, [this] { 
            return !queue_.empty() || stop_; 
        });
        
        if (stop_ && queue_.empty()) return -1;
        
        int value = queue_.front();
        queue_.pop();
        std::cout << "Consumed: " << value << std::endl;
        cv_producer_.notify_one();
        return value;
    }

    void stop() {
        std::lock_guard<std::mutex> lock(mtx_);
        stop_ = true;
        cv_producer_.notify_all();
        cv_consumer_.notify_all();
    }
};

int main() {
    ThreadSafeQueue queue;
    
    std::thread producer([&queue] {
        for (int i = 0; i < 10; ++i) {
            queue.push(i);
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
        queue.stop();
    });
    
    std::thread consumer([&queue] {
        while (true) {
            int value = queue.pop();
            if (value == -1) break;
            std::this_thread::sleep_for(std::chrono::milliseconds(150));
        }
    });
    
    producer.join();
    consumer.join();
    
    return 0;
}

5. 重要注意事项和最佳实践

  1. 虚假唤醒 (Spurious Wakeup):线程可能在没有收到通知的情况下被唤醒,因此必须使用谓词检查条件。

  2. 优先使用 std::lock_guardstd::unique_lock :避免手动调用 lock()/unlock()

  3. 在通知前解锁 :在调用 notify_one()notify_all() 前解锁,可以让被唤醒的线程立即获取锁,提高性能。

  4. 使用 RAII:确保异常安全,所有资源都能正确释放。

  5. 避免嵌套锁:容易导致死锁。

总结对比

特性 互斥锁 (Mutex) 条件变量 (Condition Variable)
主要目的 保护共享数据,避免数据竞争 线程间通信和协调
操作 lock(), unlock() wait(), notify_one(), notify_all()
配合使用 可以单独使用 必须与互斥锁配合使用
阻塞原因 等待获取锁 等待某个条件成立
典型模式 临界区保护 生产者-消费者

掌握互斥锁和条件变量是编写正确、高效多线程 C++ 程序的基础。

相关推荐
天若有情67318 小时前
C++革命性新特性:默认实例导出(exportDefault)让单例模式变得无比简单!
c++·后端
科大饭桶21 小时前
C++入门自学Day17-- 模版进阶知识
c语言·开发语言·c++·容器
91刘仁德21 小时前
c++ 类和对象(上)
开发语言·c++·经验分享·笔记·算法
阿捏利1 天前
C++ Primer Plus 第六版 第二章 编程题
c++·编程题·c++ primer plus
郝学胜-神的一滴1 天前
Pomian语言处理器研发笔记(二):使用组合模式定义表示程序结构的语法树
开发语言·c++·笔记·程序人生·决策树·设计模式·组合模式
青瓦梦滋1 天前
Linux基本工具(yum、vim、gcc、Makefile、git、gdb)
linux·运维·服务器·c++
qq_433554541 天前
C++ Bellman-Ford算法
开发语言·c++·算法
ajassi20001 天前
开源 C++ QT Widget 开发(十)IPC进程间通信--共享内存
linux·c++·qt·开源
忆萧1 天前
C++智能指针
c++