本章介绍了 C++11 引入的并发 API,重点讲解了基于任务的编程模型、线程管理、以及线程间通信的最佳实践。

条款三十五:优先考虑基于任务的编程而非基于线程的编程

推荐使用 std::async(基于任务)而非 std::thread(基于线程)。

  • 基于线程 (std::thread) 的问题

    • 无法直接获取返回值:如果异步任务有返回值,std::thread 没有直接机制获取,通常需要全局变量或复杂的同步机制。

    • 异常处理困难:如果线程函数抛出异常,std::terminate 会被调用,导致程序崩溃,难以捕获异常。

    • 资源管理复杂:需手动管理线程耗尽、资源超额(oversubscription)和负载均衡问题。

  • 基于任务 (std::async) 的优势

    • 自动获取返回值和异常:返回的 std::future 提供了 get() 函数,既可以获取返回值,也可以重新抛出任务中发生的异常。

    • 抽象层次更高:将线程管理的细节交给标准库。标准库可以根据系统负载决定是创建新线程还是利用现有线程(甚至推迟执行),从而避免资源超额。

  • 适用场景:绝大多数情况下首选 std::async。仅当需要访问底层线程 API(如设置优先级)、极度优化线程使用或实现自定义线程池时才使用 std::thread

条款三十六:如果有异步的必要请指定 std::launch::async

std::async 的默认启动策略可能不符合预期。

  • 默认策略std::launch::async | std::launch::deferred。这意味着运行时系统可以自由选择是异步执行(创建新线程)还是延迟执行(在调用 getwait 时在当前线程执行)。

  • 潜在风险

    • 无法保证并发:如果系统选择延迟执行,任务将不会并发运行。

    • 线程本地存储 (TLS) 不确定性:任务可能在当前线程或新线程运行,导致 TLS 访问变得不可预测。

    • wait_for 超时陷阱:对于延迟执行的任务,wait_for 会返回 std::future_status::deferred,如果循环等待 ready 状态,会导致无限循环。

  • 建议:如果任务必须异步执行,请显式指定 std::launch::async 策略。

条款三十七:使 std::thread 在所有路径最后都不可结合

std::thread 的析构行为极其严格,必须小心处理。

  • 可结合性 (Joinability):正在运行或已执行完但未 join/detach 的线程是可结合的。

  • 析构陷阱:如果 std::thread 对象在销毁时仍处于可结合状态,程序会调用 std::terminate 终止。

  • 解决方案:必须确保在所有路径(包括异常路径)上,std::thread 变为不可结合(调用 joindetach)。

  • RAII 封装:使用 RAII 类(如自定义的 ThreadRAII 或 C++20 的 std::jthread)在析构函数中自动处理 join 或 detach。通常建议析构时调用 join 以避免后台线程访问已销毁的局部变量。

条款三十八:关注不同线程句柄的析构行为

std::future 的析构行为有时会阻塞,有时不会。

  • 正常行为:销毁 std::future 只是释放对共享状态的引用,不阻塞

  • 例外行为(隐式 join):当且仅当满足以下三个条件时,std::future 的析构函数会阻塞等待任务完成:

    1. 关联的共享状态由 std::async 创建。

    2. 任务启动策略是 std::launch::async

    3. future 是最后一个引用共享状态的 future

  • 其他情况:来自 std::packaged_taskstd::promisefuture 析构时绝不阻塞。

条款三十九:对于一次性事件通信考虑使用 voidfutures

线程间通信有多种方式,对于一次性事件通知,void future 是一个优雅的选择。

  • 条件变量 (std::condition_variable):适合重复通知,但需要互斥锁,容易出现虚假唤醒,且如果在 wait 之前 notify 会导致信号丢失。

  • 标志位 (std::atomic<bool>):避免了互斥锁,但接收方需要轮询(浪费 CPU)。

  • Void Future (std::promise<void> + std::future<void>)

    • 机制:检测方调用 promise::set_value(),反应方调用 future::wait()

    • 优势:无须互斥锁,无轮询开销,无虚假唤醒,且信号不会丢失(无论 set_valuewait 之前还是之后调用都有效)。

    • 限制:是一次性的,只能通信一次。会有共享状态的堆内存分配开销。

    • 典型应用:让线程在创建后挂起,直到主线程完成配置(如设置优先级)后再启动。

条款四十:对于并发使用 std::atomic,对于特殊内存使用 volatile

澄清 volatilestd::atomic 的区别。

  • std::atomic

    • 用途:多线程并发编程。

    • 保证:提供原子性(操作不可分割)和顺序一致性(限制指令重排),防止数据竞争。

  • volatile

    • 用途:访问特殊内存(如内存映射 I/O)。

    • 保证:防止编译器优化掉对该变量的读写操作。

    • 局限不保证原子性,也不保证线程间的内存顺序(非线程安全),因此不能用于并发同步。

  • 结合使用volatile std::atomic<int> 可用于并发访问内存映射 I/O 的场景。