C++ Concurrency in Action(中文版)笔记
基本信息
- 书名:C++ Concurrency in Action:Practical Multithreading
- 作者:Anthony Williams(英国标准委员会BSI C++小组成员)
- 原书第2版:C++11/C++14标准
- 中文版名称:C++ Concurrency in Action(中文版-高清)可不看.pdf
- 页数:527页
- 文件大小:4.69MB
- PDF类型:mixed(文本+少量扫描页)
- 云盘路径:/books/c++/C++ Concurrency In Action(中文版-高清)可不看.pdf
核心主题概览
本书是C++并发编程的经典权威著作,系统讲解了C++11引入的标准线程库和并发机制。全书分为四个层次:
- 基础工具层(第1-4章):线程管理、共享数据保护、同步操作、期望/异步
- 底层机制层(第5章):内存模型与原子操作
- 高级构造层(第6-7章):基于锁和无锁并发数据结构设计
- 工程实践层(第8-10章):并发代码设计、线程池管理、测试调试
第1章:你好,C++的并发世界
1.1 并发与多线程的概念
并发(Concurrency):两个或更多独立的活动同时发生。
计算机系统中的并发两种形式:
- 任务切换(单核):CPU在不同任务间快速切换,造成”并发假象”
- 硬件并发(多核):真正的并行执行
并发的两种途径:
- 多进程并发:独立进程,通信复杂但安全
- 多线程并发:共享地址空间,开销小但需手动同步
1.2 为什么使用并发
两大主要原因:
| 原因 | 说明 | 示例 |
|---|---|---|
| 关注点分离(SOC) | 将不同功能分配到不同线程,降低复杂度 | DVD播放器:UI线程+解码线程 |
| 性能提升 | 利用多核并行计算 | 任务并行、数据并行 |
何时不使用并发:
- 收益不如成本(启动线程开销可能超过任务执行时间)
- 线程资源耗尽(每个线程需要独立堆栈)
- 上下文切换开销过大
- 代码复杂度增加导致维护困难
1.3 C++并发历史
- C++98:不承认线程存在,无统一内存模型
- 平台扩展:POSIX threads、Windows API
- 类库方案:Boost.Thread、ACE
- C++11突破:标准线程库 + 统一内存模型
1.4 Hello World 并发程序
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello Concurrent World\n";
}
int main() {
std::thread t(hello); // ① 创建并启动线程
t.join(); // ② 等待线程结束
}第2章:线程管理
2.1 启动线程
基本模式:
std::thread t(do_work); // 函数指针
std::thread t(obj); // 函数对象
std::thread t([]{ ... }); // Lambda表达式(推荐)避免最头疼的解析(Most Vexing Parse):
std::thread t(background_task()); // 错误:被解析为函数声明
std::thread t((background_task())); // 正确:双括号
std::thread t{background_task()}; // 正确:统一初始化2.2 等待与分离线程
| 方法 | 用途 | 注意事项 |
|---|---|---|
join() | 等待线程完成 | 只能调用一次,异常安全需RAII封装 |
detach() | 后台运行(fire-and-forget) | 线程脱离std::thread对象控制 |
RAII线程守卫模式:
class thread_guard {
std::thread& t;
public:
explicit thread_guard(std::thread& t_) : t(t_) {}
~thread_guard() {
if (t.joinable()) t.join();
}
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const&) = delete;
};2.3 向线程传递参数
默认按值拷贝:
std::thread t(f, 3, std::string("hello"));传递引用需用 std::ref:
std::thread t(update_data, w, std::ref(data));移动语义支持:
std::unique_ptr<big_object> p(new big_object);
std::thread t(process_big_object, std::move(p));2.4 线程所有权转移
std::thread是可移动(movable)但不可拷贝(non-copyable)的类型:
std::thread t1(some_function);
std::thread t2 = std::move(t1); // 所有权转移
t1 = std::thread(some_other_function); // 临时对象隐式移动2.5 识别线程
std::thread::id master_thread = std::this_thread::get_id();
void some_core_part_of_algorithm() {
if (std::this_thread::get_id() == master_thread) {
do_master_thread_work();
}
do_common_work();
}2.6 并行算法示例
template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init) {
unsigned long const length = std::distance(first, last);
if (!length) return init;
unsigned long const min_per_thread = 25;
unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads = std::thread::hardware_concurrency();
unsigned long const num_threads = std::min(
hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::vector<T> results(num_threads);
std::vector<std::thread> threads(num_threads - 1);
Iterator block_start = first;
for (unsigned long i = 0; i < (num_threads - 1); ++i) {
Iterator block_end = block_start;
std::advance(block_end, block_size);
threads[i] = std::thread(
accumulate_block<Iterator, T>(),
block_start, block_end, std::ref(results[i]));
block_start = block_end;
}
accumulate_block<Iterator, T>()(
block_start, last, results[num_threads - 1]);
std::for_each(threads.begin(), threads.end(),
std::mem_fn(&std::thread::join));
return std::accumulate(results.begin(), results.end(), init);
}第3章:线程间共享数据
3.1 共享数据的问题
核心问题:条件竞争(Race Condition)
- 多线程访问共享数据时,执行顺序影响结果
- 破坏数据结构不变量(如双链表指针)
- 数据竞争(Data Race)是未定义行为
3.2 互斥量保护数据
基本用法:
std::mutex some_mutex;
std::lock_guard<std::mutex> guard(some_mutex);
// RAII:析构时自动解锁保护共享数据的准则:
- 互斥量应与被保护数据放在同一类中
- 不通过返回值或参数传出受保护数据的引用
- 接口设计要原子化,避免复合操作的条件竞争
3.3 接口固有的条件竞争
问题示例:线程安全栈
if (!s.empty()) { // 竞争点1
int const value = s.top(); // 竞争点2
s.pop();
}解决方案:合并操作
// 方案1:传入引用
void pop(T& value);
// 方案2:返回shared_ptr(异常安全)
std::shared_ptr<T> pop();3.4 死锁问题
死锁条件:
- 互斥量以不同顺序获取
- 嵌套锁(持有锁A等待锁B)
解决方案:
- 固定顺序获取锁
- std::lock()一次性获取多个锁
std::lock(lhs.m, rhs.m);
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);- 锁的层次结构(hierarchical_mutex)
hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);
// 高层级锁可获取低层级锁,反之会抛出异常3.5 std::unique_lock vs std::lock_guard
| 特性 | std::lock_guard | std::unique_lock |
|---|---|---|
| 灵活性 | 低 | 高 |
| 延迟锁 | 不支持 | 支持 |
| 锁转移 | 不支持 | 支持 |
| 性能 | 略优 | 略差 |
3.6 保护共享数据的替代方案
延迟初始化:std::call_once
std::once_flag init_flag;
std::shared_ptr<some_resource> resource_ptr;
void init_resource() {
resource_ptr.reset(new some_resource);
}
void foo() {
std::call_once(init_flag, init_resource);
resource_ptr->do_something();
}读写锁:boost::shared_mutex
boost::shared_mutex entry_mutex;
// 读操作:共享锁
boost::shared_lock<boost::shared_mutex> lk(entry_mutex);
// 写操作:独占锁
std::lock_guard<boost::shared_mutex> lk(entry_mutex);第4章:同步并发操作
4.1 条件变量
基本用法:
std::condition_variable data_cond;
std::mutex mut;
std::queue<data_chunk> data_queue;
// 生产者
void data_preparation_thread() {
while (more_data_to_prepare()) {
data_chunk const data = prepare_data();
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
}
// 消费者
void data_processing_thread() {
while (true) {
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, []{ return !data_queue.empty(); });
data_chunk data = data_queue.front();
data_queue.pop();
lk.unlock();
process(data);
}
}线程安全队列实现要点:
- push/wait_and_pop/try_pop/empty 四个操作
- 使用
std::shared_ptr<T>返回弹出值(异常安全) - 拷贝构造函数需要对互斥量上锁
4.2 期望(Future)与异步
std::async 启动异步任务:
std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
// ... 做其他事情 ...
int result = the_answer.get(); // 阻塞直到结果就绪std::launch策略:
auto f1 = std::async(std::launch::async, Y(), 1.2); // 必须在新线程执行
auto f2 = std::async(std::launch::deferred, baz, ref(x)); // 延迟到wait()/get()时执行
auto f3 = std::async(baz, ref(x)); // 实现自选(默认)4.3 打包任务(packaged_task)
std::deque<std::packaged_task<void()>> tasks;
std::mutex m;
// GUI线程:处理任务队列
void gui_thread() {
while (!gui_shutdown_message_received()) {
get_and_process_gui_message();
std::packaged_task<void()> task;
{
std::lock_guard<std::mutex> lk(m);
if (tasks.empty()) continue;
task = std::move(tasks.front());
tasks.pop_front();
}
task(); // 执行任务
}
}
// 提交任务
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f) {
std::packaged_task<void()> task(f);
std::future<void> res = task.get_future();
{
std::lock_guard<std::mutex> lk(m);
tasks.push_back(std::move(task));
}
return res;
}第5章:C++内存模型与原子操作
5.1 内存模型基础
关键概念:
- 修改顺序(Modification Order):每个原子变量有一个全局修改序列
- 关系排序(Happens-Before):定义操作之间的可见性保证
5.2 内存序(Memory Orders)
| 内存序 | 含义 | 适用场景 |
|---|---|---|
memory_order_relaxed | 无同步保证 | 计数器、统计信息 |
memory_order_consume | 依赖链传递 | 指针加载 |
memory_order_acquire | 获取语义 | 读取共享数据前 |
memory_order_release | 释放语义 | 写入共享数据后 |
memory_order_acq_rel | 获取+释放 | 读-改-写操作 |
memory_order_seq_cst | 顺序一致(默认) | 需要强一致性 |
5.3 原子操作示例
std::atomic<int> counter{0};
// 无原子保证的操作(危险!)
int read_counter() { return counter; } // 需要atomic load
// 正确的原子操作
counter.fetch_add(1, std::memory_order_relaxed);第6章:基于锁的并发数据结构
6.1 锁粒度选择
- 粗粒度锁:简单但竞争激烈
- 细粒度锁:性能好但复杂
6.2 分段锁(Lock Stripping)
将数据结构分成多段,每段独立加锁:
template<typename T>
class split_lock_queue {
static constexpr size_t NUM_LOCKS = 16;
std::array<std::mutex, NUM_LOCKS> locks;
std::array<std::queue<T>, NUM_LOCKS> queues;
size_t get_lock_index(size_t id) {
return id % NUM_LOCKS;
}
};第7章:无锁并发数据结构
7.1 无锁编程基础
CAS(Compare-And-Swap)操作:
std::atomic<bool> lock{false};
void acquire() {
while (lock.exchange(true, std::memory_order_acq_rel)) {
// 自旋等待
}
}
void release() {
lock.store(false, std::memory_order_release);
}7.2 ABA问题
问题描述:
- 线程1读取值为A
- 线程2将值改为B再改回A
- 线程1的CAS检查通过,但状态已变
解决方案:
- 使用带版本的指针(tagged pointer)
- 使用 hazard pointer 等内存回收机制
第8章:并发代码设计
8.1 并行算法设计
任务并行 vs 数据并行:
- 任务并行:不同线程执行不同任务
- 数据并行:不同线程处理数据的不同部分
8.2 性能优化技巧
- 减少锁竞争:使用无锁结构或细粒度锁
- 避免虚假共享:对齐数据结构到缓存行边界
- 合理线程数:不超过硬件并发数
- 局部性优化:线程处理连续内存区域
第9章:高级线程管理
9.1 线程池
基本组件:
- 工作线程队列
- 任务队列
- 任务提交接口
class thread_pool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
public:
thread_pool(size_t threads) : stop(false) {
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();
}
});
}
}
template<typename F>
std::future<typename std::result_of<F()>::type> submit(F f) {
using return_type = typename std::result_of<F()>::type;
auto task = std::make_shared<std::packaged_task<return_type()>>(f);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace([task]() { (*task)(); });
}
condition.notify_one();
return res;
}
};9.2 中断线程
协作式中断:
void worker_thread(std::atomic<bool>& stop_flag) {
while (!stop_flag.load(std::memory_order_relaxed)) {
// 执行工作
}
}第10章:多线程程序的测试和调试
10.1 Bug类型
| 类型 | 特点 | 示例 |
|---|---|---|
| 条件竞争 | 结果依赖执行顺序 | 双链表删除 |
| 死锁 | 线程互相等待 | 锁顺序不一致 |
| 活锁 | 线程不断重试但无进展 | 退避算法失效 |
| 饥饿 | 线程无法获取资源 | 优先级反转 |
10.2 调试工具
- ThreadSanitizer(TSan):编译时插入检测代码
- Helgrind:Valgrind工具集
- 静态分析工具:Coverity、PVS-Studio
附录D:C++线程类库参考速查
核心类
| 类/函数 | 头文件 | 用途 |
|---|---|---|
std::thread | <thread> | 线程管理 |
std::mutex | <mutex> | 互斥量 |
std::lock_guard | <mutex> | RAII锁管理 |
std::unique_lock | <mutex> | 灵活锁管理 |
std::condition_variable | <condition_variable> | 条件等待 |
std::future | <future> | 异步结果 |
std::promise | <future> | 设置异步值 |
std::packaged_task | <future> | 任务包装 |
std::async | <future> | 异步启动 |
std::atomic | <atomic> | 原子操作 |
std::call_once | <mutex> | 一次性初始化 |
常用模式
RAII锁:
std::mutex m;
{
std::lock_guard<std::mutex> lock(m);
// 临界区
}
// 自动解锁延迟锁:
std::mutex m;
std::unique_lock<std::mutex> lock(m, std::defer_lock);
// 后续操作...
lock.lock();
// 或使用 std::lock()
std::lock(m1, m2);
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);实践建议总结
何时使用并发
- ✅ 多核处理器充分利用
- ✅ 隐藏I/O延迟
- ✅ 分离关注点(UI + 业务逻辑)
- ❌ 简单顺序任务
- ❌ 线程开销大于收益
- ❌ 代码复杂度不可控
最佳实践
- 优先使用高级抽象:
std::async、std::future、线程池 - 最小化临界区:锁住操作尽可能短
- 避免锁嵌套:或使用
std::lock() - 使用线程安全数据结构:而非裸指针+手动同步
- 测试并发代码:使用 ThreadSanitizer 等工具
- 文档化线程契约:说明哪些数据需要保护
与其他知识的关联
- Effective Modern C++ - Scott Meyers的现代C++实践
- A Tour of C++ 2nd Edition - Bjarne Stroustrup的C++概览
- C++ Concurrency in Action - 本笔记对应书籍的英文版
- 并发编程的艺术 - Java并发编程参考
- 深入理解计算机系统 - 计算机组成与并发基础
笔记整理日期:2026-09-03 来源:阿里云盘 /books/c++/C++ Concurrency In Action(中文版-高清)可不看.pdf