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. 基础工具层(第1-4章):线程管理、共享数据保护、同步操作、期望/异步
  2. 底层机制层(第5章):内存模型与原子操作
  3. 高级构造层(第6-7章):基于锁和无锁并发数据结构设计
  4. 工程实践层(第8-10章):并发代码设计、线程池管理、测试调试

第1章:你好,C++的并发世界

1.1 并发与多线程的概念

并发(Concurrency):两个或更多独立的活动同时发生。

计算机系统中的并发两种形式:

  • 任务切换(单核):CPU在不同任务间快速切换,造成”并发假象”
  • 硬件并发(多核):真正的并行执行

并发的两种途径:

  1. 多进程并发:独立进程,通信复杂但安全
  2. 多线程并发:共享地址空间,开销小但需手动同步

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:析构时自动解锁

保护共享数据的准则:

  1. 互斥量应与被保护数据放在同一类中
  2. 不通过返回值或参数传出受保护数据的引用
  3. 接口设计要原子化,避免复合操作的条件竞争

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)

解决方案:

  1. 固定顺序获取锁
  2. 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);
  1. 锁的层次结构(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_guardstd::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 性能优化技巧

  1. 减少锁竞争:使用无锁结构或细粒度锁
  2. 避免虚假共享:对齐数据结构到缓存行边界
  3. 合理线程数:不超过硬件并发数
  4. 局部性优化:线程处理连续内存区域

第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 + 业务逻辑)
  • ❌ 简单顺序任务
  • ❌ 线程开销大于收益
  • ❌ 代码复杂度不可控

最佳实践

  1. 优先使用高级抽象:std::async、std::future、线程池
  2. 最小化临界区:锁住操作尽可能短
  3. 避免锁嵌套:或使用 std::lock()
  4. 使用线程安全数据结构:而非裸指针+手动同步
  5. 测试并发代码:使用 ThreadSanitizer 等工具
  6. 文档化线程契约:说明哪些数据需要保护

与其他知识的关联


笔记整理日期:2026-09-03 来源:阿里云盘 /books/c++/C++ Concurrency In Action(中文版-高清)可不看.pdf