本章介绍了 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。这意味着运行时系统可以自由选择是异步执行(创建新线程)还是延迟执行(在调用get或wait时在当前线程执行)。潜在风险:
无法保证并发:如果系统选择延迟执行,任务将不会并发运行。
线程本地存储 (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变为不可结合(调用join或detach)。RAII 封装:使用 RAII 类(如自定义的
ThreadRAII或 C++20 的std::jthread)在析构函数中自动处理 join 或 detach。通常建议析构时调用join以避免后台线程访问已销毁的局部变量。
条款三十八:关注不同线程句柄的析构行为
std::future 的析构行为有时会阻塞,有时不会。
正常行为:销毁
std::future只是释放对共享状态的引用,不阻塞。例外行为(隐式 join):当且仅当满足以下三个条件时,
std::future的析构函数会阻塞等待任务完成:关联的共享状态由
std::async创建。任务启动策略是
std::launch::async。该
future是最后一个引用共享状态的future。
其他情况:来自
std::packaged_task或std::promise的future析构时绝不阻塞。
条款三十九:对于一次性事件通信考虑使用 void 的 futures
线程间通信有多种方式,对于一次性事件通知,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_value在wait之前还是之后调用都有效)。限制:是一次性的,只能通信一次。会有共享状态的堆内存分配开销。
典型应用:让线程在创建后挂起,直到主线程完成配置(如设置优先级)后再启动。
条款四十:对于并发使用 std::atomic,对于特殊内存使用 volatile
澄清 volatile 和 std::atomic 的区别。
std::atomic:用途:多线程并发编程。
保证:提供原子性(操作不可分割)和顺序一致性(限制指令重排),防止数据竞争。
volatile:用途:访问特殊内存(如内存映射 I/O)。
保证:防止编译器优化掉对该变量的读写操作。
局限:不保证原子性,也不保证线程间的内存顺序(非线程安全),因此不能用于并发同步。
结合使用:
volatile std::atomic<int>可用于并发访问内存映射 I/O 的场景。
评论