1. C++11线程库概述
在C++11标准之前,多线程编程主要依赖于平台特定的API,如Linux下的pthread库。这种实现方式虽然功能强大,但存在明显的跨平台兼容性问题。C++11标准首次将线程支持纳入标准库,提供了一套类型安全、跨平台的并发编程接口。
1.1 为什么需要标准线程库
传统多线程开发面临三大痛点:
- 平台依赖性:不同操作系统提供的线程API差异大
- 类型不安全:C风格API容易引发类型错误
- 管理复杂:手动管理线程生命周期容易导致资源泄漏
C++11线程库通过RAII(资源获取即初始化)机制和面向对象设计,有效解决了这些问题。例如,当线程对象离开作用域时,如果仍可join,标准库会调用std::terminate()防止资源泄漏。
重要提示:虽然标准库简化了线程操作,但多线程编程本身的复杂性(如竞态条件、死锁等)仍然存在,需要开发者谨慎处理。
2. 线程创建与管理
2.1 thread类基础用法
std::thread是C++11线程库的核心类,定义在<thread>头文件中。创建线程的基本语法如下:
cpp复制#include <thread>
#include <iostream>
void hello() {
std::cout << "Hello from thread!\n";
}
int main() {
std::thread t(hello);
t.join();
return 0;
}
线程创建时需要注意:
- 线程对象构造时立即开始执行
- 必须明确决定是join()还是detach()
- 线程函数可以通过任何可调用对象实现
2.2 线程参数传递
向线程函数传递参数时,参数会默认被拷贝到线程的独立存储空间。如果需要传递引用,必须使用std::ref包装:
cpp复制void modify(int& x) {
x = 42;
}
int main() {
int value = 0;
std::thread t(modify, std::ref(value));
t.join();
std::cout << value; // 输出42
return 0;
}
2.3 线程生命周期管理
C++11提供了多种线程控制方法:
| 方法 | 描述 | 使用场景 |
|---|---|---|
| join() | 阻塞当前线程直到目标线程完成 | 需要等待线程结果时 |
| detach() | 分离线程,使其独立运行 | 后台任务,不关心结果时 |
| joinable() | 检查线程是否可join | 判断线程状态 |
常见错误:忘记join或detach会导致程序终止。建议使用RAII包装器自动管理线程生命周期。
3. 互斥量与锁机制
3.1 mutex基础
C++11提供了四种互斥量类型:
std::mutex:基本互斥量std::recursive_mutex:可重入互斥量std::timed_mutex:带超时的互斥量std::recursive_timed_mutex:可重入且带超时的互斥量
基本用法示例:
cpp复制std::mutex mtx;
int shared_data = 0;
void increment() {
mtx.lock();
++shared_data;
mtx.unlock();
}
3.2 RAII锁包装器
手动管理锁的获取和释放容易出错,C++11提供了两种RAII风格的锁管理类:
std::lock_guard:简单的作用域锁
cpp复制{
std::lock_guard<std::mutex> lock(mtx);
// 临界区代码
} // 自动释放锁
std::unique_lock:更灵活的锁管理
cpp复制std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
lock.lock(); // 可以手动控制锁定时机
// ...
lock.unlock(); // 可以手动释放
3.3 锁的性能考量
不同锁策略的性能特点:
| 锁类型 | 开销 | 适用场景 |
|---|---|---|
| mutex | 中等 | 一般情况 |
| spinlock | 高CPU但低延迟 | 短期等待 |
| recursive_mutex | 较高 | 递归调用 |
经验法则:临界区执行时间短(<1μs)考虑自旋锁,否则用互斥锁。C++标准库没有提供自旋锁,但可以用atomic实现。
4. 条件变量
4.1 基本用法
条件变量(std::condition_variable)用于线程间通信,典型的生产者-消费者模式实现:
cpp复制std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
// 处理数据
}
void producer() {
std::lock_guard<std::mutex> lock(mtx);
ready = true;
cv.notify_one();
}
4.2 虚假唤醒问题
条件变量的wait操作应该总是使用谓词版本,以防止虚假唤醒:
cpp复制cv.wait(lock, []{ return data_ready; }); // 正确
while (!data_ready) cv.wait(lock); // 也可以
4.3 通知策略比较
| 方法 | 描述 | 使用场景 |
|---|---|---|
| notify_one() | 唤醒一个等待线程 | 单个消费者 |
| notify_all() | 唤醒所有等待线程 | 多个消费者 |
5. 原子操作
5.1 atomic类型
std::atomic模板提供了原子变量支持:
cpp复制std::atomic<int> counter{0};
void increment() {
counter.fetch_add(1, std::memory_order_relaxed);
}
5.2 内存顺序
C++11定义了6种内存顺序,常用的有:
| 顺序 | 描述 | 开销 |
|---|---|---|
| seq_cst | 顺序一致性 | 高 |
| acquire-release | 获取-释放语义 | 中 |
| relaxed | 无顺序保证 | 低 |
5.3 无锁编程模式
原子操作常用于实现无锁数据结构。例如简单的无锁栈:
cpp复制template<typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head;
public:
void push(const T& data) {
Node* new_node = new Node{data, head.load()};
while(!head.compare_exchange_weak(new_node->next, new_node));
}
// ...
};
6. 线程池实现模式
虽然标准库没有直接提供线程池,但可以用这些组件构建:
cpp复制class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop = false;
public:
ThreadPool(size_t threads) {
for(size_t i = 0; i < threads; ++i)
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this]{ return stop || !tasks.empty(); });
if(stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
// ...
};
7. 性能优化技巧
-
减少锁竞争:
- 使用细粒度锁
- 考虑读写锁(C++14的shared_mutex)
- 使用线程本地存储
-
缓存友好设计:
- 避免false sharing(用alignas或padding)
- 合理安排数据布局
-
异步编程模式:
- 结合future/promise
- 使用async启动异步任务
8. 常见问题排查
-
死锁检测:
- 按固定顺序获取锁
- 使用std::lock同时锁定多个互斥量
- 考虑使用层次锁
-
性能瓶颈分析:
- 使用profiler工具
- 检查锁竞争情况
- 评估原子操作开销
-
线程安全保证:
- 区分线程安全、可重入、不可重入
- 注意静态变量的初始化
9. 最佳实践总结
-
资源管理:
- 优先使用RAII包装器
- 避免裸线程对象
- 明确所有权转移
-
错误处理:
- 捕获线程函数异常
- 使用future传递异常
- 实现优雅终止
-
调试技巧:
- 给线程命名(平台相关)
- 使用thread_local存储调试信息
- 实现死锁检测机制
在实际项目中,我通常会先评估是否真的需要多线程。对于IO密集型任务,异步IO可能更高效;对于计算密集型任务,考虑任务并行度与硬件核心数的匹配。C++17引入的并行算法和C++20的协程也提供了更多并发编程的选择。
