C++ 并发编程
一、并发基础核心组件
口诀:线程对象要管理,互斥锁来保安全,原子操作不可拆,条件变量通信忙
1.1 std::thread - 线程管理
- 构造方式:
- 默认构造:创建空线程对象
- 初始化构造:
thread t(func, args...)
,传入函数和参数 - 移动构造:支持线程所有权转移,不支持拷贝
- 生命周期管理:
join()
:主线程等待子线程完成,阻塞当前线程detach()
:线程独立运行,与主线程分离- 关键注意:线程对象销毁前必须调用join()或detach(),否则程序崩溃
- 常见陷阱:局部线程对象销毁时,若线程函数仍在运行会导致崩溃(解决:使用detach()或确保join()被调用)
1.2 std::mutex - 互斥锁
- 基本功能:保护共享资源,确保同一时间只有一个线程访问
- 核心方法:
lock()
(加锁)、unlock()
(解锁)、try_lock()
(尝试加锁,非阻塞) - 使用建议:配合RAII包装器使用,避免忘记解锁导致死锁
1.3 std::lock_guard - RAII锁管理
- 核心特性:RAII风格的锁管理,构造时自动加锁,析构时自动解锁
- 优点:避免忘记解锁,防止异常导致的死锁
- 限制:作用域内持续锁定,无法中途解锁,不可复制
1.4 std::unique_lock - 灵活锁管理
- 核心特性:lock_guard的升级版,提供更多灵活性
- 主要优势:
- 支持延迟锁定:
unique_lock(mtx, defer_lock)
- 可随时加锁解锁:
lock()
,unlock()
- 可移动,不可复制
- 支持条件变量(必须使用unique_lock)
- 支持延迟锁定:
二、线程创建与管理详解
口诀:线程创建传函数,join等待detach离,现代推荐jthread,自动join更安全
2.1 线程创建方法
- 函数指针:
std::thread t(func, args...)
- Lambda表达式:
std::thread t([]{ /* 线程代码 */ });
- 类成员函数:
std::thread t(&Class::method, &obj, args...)
2.2 join() vs detach() 选择
- join()适用场景:需要等待线程完成,依赖其执行结果
- detach()适用场景:后台任务、日志记录、监控等无需主线程等待的情况
- 注意:detach()后线程由运行时管理,无法获取状态或结果
2.3 现代C++线程管理
- C++20 std::jthread:自动在析构时调用join(),避免忘记管理的风险
- std::async:更高级的异步任务接口,自动管理线程和返回结果
1
2std::future<int> result = std::async(std::launch::async, add, 5, 3);
int val = result.get(); // 阻塞等待结果
三、lock_guard与unique_lock对比
口诀:lock_guard自动锁,作用域内不解锁;unique_lock更灵活,条件变量配合多
特性 | lock_guard | unique_lock |
---|---|---|
自动锁定解锁 | ✓ | ✓ |
中途解锁 | ✗ | ✓ |
延迟锁定 | ✗ | ✓ |
移动语义 | ✗ | ✓ |
条件变量支持 | ✗ | ✓ |
适用场景 | 简单临界区 | 复杂同步场景 |
四、std::atomic - 原子操作
口诀:原子操作不可拆,无需锁也能同步,适用于简单计数器,性能更高更安全
核心概念:确保对变量的操作是不可分割的,要么全部完成,要么完全不执行
适用场景:计数器、标志位、引用计数等简单共享变量
常用操作:
fetch_add()
、fetch_sub()
、load()
、store()
优势:无需互斥锁,避免上下文切换开销,性能更高
注意:复杂逻辑仍需互斥锁保护,atomic仅保证单个操作的原子性
1
2std::atomic<int> counter(0);
void increment() { counter.fetch_add(1); } // 原子递增
五、多线程同步机制详解
口诀:互斥锁保临界区,条件变量通信忙,原子操作性能高,信号量控资源量
5.1 互斥锁(std::mutex)
- 作用:保护临界区,确保同一时间只有一个线程访问共享资源
- 使用方式:配合lock_guard或unique_lock使用
1
2
3
4
5std::mutex mtx;
void critical_section() {
std::lock_guard<std::mutex> lock(mtx);
// 共享资源操作
}
5.2 条件变量(std::condition_variable)
- 作用:线程间通信,等待特定条件满足后继续执行
- 核心方法:
wait(lock, predicate)
:等待条件,可带谓词notify_one()
:唤醒一个等待线程notify_all()
:唤醒所有等待线程
- 使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void wait_for_ready() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待ready变为true
// 条件满足后的操作
}
void set_ready() {
std::lock_guard<std::mutex> lock(mtx);
ready = true;
cv.notify_all(); // 通知所有等待线程
}
5.3 信号量(std::counting_semaphore)
- 作用:控制对共享资源的并发访问数量
- 核心方法:
acquire()
:获取资源,若资源不足则阻塞release()
:释放资源,增加可用资源数量
- 使用示例:
1
2
3
4
5
6
7std::counting_semaphore<10> sem(10); // 最多10个线程同时访问
void access_resource() {
sem.acquire(); // 获取许可
// 访问共享资源
sem.release(); // 释放许可
}
六、线程池实现思路
口诀:任务队列来存储,工作线程池中取,互斥锁加条件量,线程管理自动化
6.1 核心组件
- 任务队列:
std::queue<std::function<void()>>
存储待执行任务 - 工作线程池:
std::vector<std::thread>
保存工作线程 - 同步机制:
std::mutex
:保护任务队列访问std::condition_variable
:通知等待的线程std::atomic<bool>
:线程池关闭标志
6.2 实现流程
- 初始化:创建指定数量的工作线程,每个线程进入循环
- 工作线程逻辑:
- 获取互斥锁
- 检查任务队列是否为空,为空则等待条件变量
- 非空则取出任务并执行
- 循环直到收到关闭信号
- 任务提交:将任务添加到队列,通知等待的线程
- 关闭:设置关闭标志,通知所有线程,等待线程结束
6.3 核心代码逻辑
1 | // 工作线程循环的核心逻辑 |
七、多线程编程最佳实践
口诀:避免共享数据,必须共享则同步,优先使用RAII锁,死锁预防是关键
7.1 死锁预防策略
- 按固定顺序获取锁,避免循环等待
- 使用
std::lock(mtx1, mtx2)
同时锁定多个互斥量 - 配合
lock_guard
的adopt_lock
标签使用 - 避免在持有锁时调用未知函数
- 设置锁的超时机制(如使用
std::timed_mutex
)
7.2 性能优化技巧
- 减少锁的粒度,仅保护必要的共享数据
- 优先使用原子操作代替互斥锁(简单场景)
- 使用
std::shared_mutex
实现读写锁(读多写少场景) - 利用
thread_local
避免数据竞争 - 根据硬件并发数调整线程数量:
std::thread::hardware_concurrency()
7.3 现代C++并发工具
C++17 std::shared_mutex:读写锁,允许多个读线程同时访问
C++20 std::jthread:自动join的线程,更安全
C++20 std::latch / std::barrier:线程同步点控制
C++11 std::future / std::promise:异步任务和结果获取
C++17 std::shared_future:允许多个线程共享future结果
总结:C++并发编程的核心在于正确管理线程生命周期和同步共享资源。通过掌握std::thread、互斥锁、条件变量、原子操作等基础工具,结合RAII原则和现代C++特性,可以编写出高效、安全的多线程程序。重点记忆线程管理规则、锁的使用场景区别以及死锁预防策略,能够有效避免常见的并发编程陷阱。
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.