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; // 正确输出 200000return 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; }
条件变量的关键方法
wait(lock, predicate)
:原子地解锁并阻塞当前线程
被唤醒后重新获取锁
检查 predicate 条件,如果为 false 则继续等待
notify_one()
:唤醒一个等待中的线程(如果有)
notify_all()
:唤醒所有等待中的线程
3. 为什么条件变量需要互斥锁?
条件变量必须与互斥锁配合使用,原因如下:
原子性操作:检查条件和进入等待必须是原子操作,否则可能发生:
线程A检查条件 → 条件不满足
线程B修改条件并发出通知
线程A才开始等待 → 通知丢失,线程A永远等待
保护共享状态:条件变量等待的"条件"通常是共享数据,需要用互斥锁保护。
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. 重要注意事项和最佳实践
虚假唤醒 (Spurious Wakeup):线程可能在没有收到通知的情况下被唤醒,因此必须使用谓词检查条件。
优先使用
std::lock_guard
和std::unique_lock
:避免手动调用lock()
/unlock()
。在通知前解锁:在调用
notify_one()
或notify_all()
前解锁,可以让被唤醒的线程立即获取锁,提高性能。使用 RAII:确保异常安全,所有资源都能正确释放。
避免嵌套锁:容易导致死锁。
总结对比
特性 | 互斥锁 (Mutex) | 条件变量 (Condition Variable) |
---|---|---|
主要目的 | 保护共享数据,避免数据竞争 | 线程间通信和协调 |
操作 | lock() , unlock() | wait() , notify_one() , notify_all() |
配合使用 | 可以单独使用 | 必须与互斥锁配合使用 |
阻塞原因 | 等待获取锁 | 等待某个条件成立 |
典型模式 | 临界区保护 | 生产者-消费者 |
掌握互斥锁和条件变量是编写正确、高效多线程 C++ 程序的基础。