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;
}
条件变量的关键方法
-
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++ 程序的基础。