多线程编程:std::thread 与 std::async,线程安全队列,锁与条件变量,死锁预防
多线程编程,说白了就是让程序同时干好几件事。我刚开始接触多线程时,觉得这玩意儿挺酷的——能并行执行,性能肯定翻倍。结果第一次上线就出了大问题,数据乱成一锅粥。嗯,从那以后我才明白,多线程不是简单的「开几个线程就完事」,它需要一套严谨的工程方法论。
今天我们就来聊聊 C++ 多线程编程的核心内容。我会结合自己的实战经验,把 std::thread、std::async、线程安全队列、锁与条件变量、死锁预防这些知识点串起来。
一、std::thread:最基础的线程工具
std::thread 是 C++11 引入的线程类。我个人习惯用它来执行那些「跑完就结束」的任务。比如后台日志写入、数据预处理等。
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "线程 " << id << " 开始工作\n";
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "线程 " << id << " 工作结束\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // 等待线程结束
t2.join();
return 0;
}
二、std::async:更优雅的异步任务
std::async 比 std::thread 更高级。它会返回一个 std::future 对象,你可以通过它获取线程的执行结果。说白了,std::async 就是「把任务丢出去,回头再拿结果」。
#include <future>
#include <iostream>
int compute(int x) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return x * x;
}
int main() {
std::future<int> result = std::async(std::launch::async, compute, 10);
// 干点别的事...
std::cout << "等待计算结果...\n";
// 获取结果(会阻塞直到计算完成)
int value = result.get();
std::cout << "结果是: " << value << "\n";
return 0;
}
三、线程安全队列:生产-消费模式的核心
多线程编程中,队列是最常用的数据结构之一。但标准库的 std::queue 不是线程安全的。你需要自己封装一个线程安全版本。
我在项目中遇到过这样的场景:一个线程负责接收网络数据,多个线程负责处理数据。这时候就需要一个线程安全的队列来做缓冲。
#include <queue>
#include <mutex>
#include <condition_variable>
template<typename T>
class ThreadSafeQueue {
private:
std::queue<T> queue_;
mutable std::mutex mutex_;
std::condition_variable cond_;
public:
void push(T value) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(std::move(value));
cond_.notify_one(); // 通知等待的消费者
}
T pop() {
std::unique_lock<std::mutex> lock(mutex_);
cond_.wait(lock, [this] { return !queue_.empty(); });
T value = std::move(queue_.front());
queue_.pop();
return value;
}
bool try_pop(T& value) {
std::lock_guard<std::mutex> lock(mutex_);
if (queue_.empty()) return false;
value = std::move(queue_.front());
queue_.pop();
return true;
}
};
四、锁与条件变量:线程间的「交通指挥」
锁(std::mutex)用来保护共享数据,条件变量(std::condition_variable)用来协调线程的执行顺序。两者经常配合使用。
举个例子:一个线程生产数据,另一个线程消费数据。消费者需要等生产者生产完才能消费。这时候条件变量就派上用场了。
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void producer() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
std::cout << "生产者: 数据准备好了\n";
}
cv.notify_one(); // 通知消费者
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // 等待条件满足
std::cout << "消费者: 收到数据,开始处理\n";
}
五、死锁预防:别让线程「互相等待」
死锁是多线程编程中最头疼的问题之一。说白了就是两个线程互相持有对方需要的锁,谁也不肯放手,结果大家都卡住了。
我曾经在项目中遇到过死锁:线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1。结果整个系统卡死了,线上报警响个不停。从那以后,我总结了几条死锁预防的黄金法则:
| 预防策略 | 说明 | 推荐度 |
|---|---|---|
| 固定加锁顺序 | 所有线程按相同顺序获取锁 | ⭐⭐⭐⭐⭐ |
| 使用 std::lock() | 一次性锁定多个互斥量,避免死锁 | ⭐⭐⭐⭐ |
| 避免嵌套锁 | 尽量只持有一个锁 | ⭐⭐⭐⭐ |
| 使用超时锁 | try_lock_for() 或 try_lock_until() | ⭐⭐⭐ |
// 使用 std::lock() 避免死锁
std::mutex mtx1, mtx2;
void safe_operation() {
// 一次性锁定两个互斥量
std::lock(mtx1, mtx2);
// 使用 adopt_lock 表示锁已经被锁定
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 安全地操作共享数据
// ...
}
六、实战经验总结
多线程编程没有银弹。你想想看,每个项目的情况都不一样。但有几条原则是通用的:
- 尽量使用 std::async:它比 std::thread 更安全,自动管理线程生命周期。
- 锁的粒度要小:只保护必要的代码段,不要锁住整个函数。
- 优先使用 std::lock_guard:RAII 风格,异常安全。
- 条件变量一定要配合 while 循环:防止虚假唤醒(spurious wakeup)。
- 死锁预防要提前设计:不要等到出问题了再补救。
我个人习惯在项目初期就画好线程交互图,明确每个线程的职责和锁的获取顺序。这样能避免很多后期的问题。