条款38 :对变化多端的线程句柄析构函数行为保持关注
该条款的核心论点是,std::thread 和期值(std::future / std::shared_future)虽然都可以看作是系统线程的句柄,但它们的析构函数行为却截然不同,这种差异对于编写健壮的并发代码至关重要。
对于std::thread, 正如前面所说, 其析构函数的行为非常严格和明确:如果 std::thread 对象是可联结的 (joinable),其析构函数的调用将导致程序终止
与 std::thread 不同,期值(std::future)析构函数的行为要复杂得多。为了理解它,首先需要了解共享状态 (shared state) 的概念: 共享状态是期值通信机制的核心。它通常是在堆上分配的一个对象,用于存储由异步任务(被调方)计算出的结果或抛出的异常,并将其传递给期值(调用方) 。
期值析构函数的行为完全取决于其所引用的共享状态。具体来说,有两种截然不同的行为模式。
常规行为:仅析构期值对象
对于绝大多数期值,其析构函数只会做一件事:销毁期值对象本身。这意味着它不会 join,也不会 detach,不会阻塞,也不会运行任何东西。它仅仅是销毁期值的成员变量,并递减共享状态的引用计数。这种常规行为适用于所有通过 std::promise 或 std::packaged_task 创建的期值。
特殊行为:阻塞并等待
存在一个非常重要的例外情况。当且仅当一个期值同时满足以下所有条件时,其析构函数会阻塞,直到异步任务完成(效果等同于隐式的 join):该期值所引用的共享状态是由 std::async 的调用所创建的。该任务的启动策略是 std::launch::async (无论是显式指定还是由默认策略选择的)。该期值是最后一个引用该共享状态的期值。
1 | // 析构函数可能会阻塞 |
如果这些容器或对象中持有的期值恰好是最后一个引用由 std::async 启动的异步任务的期值,那么在容器或对象析构时,程序就会在此处阻塞,直到任务完成。
特殊行为的成因与后果
成因:标准委员会为了避免隐式 detach 可能导致的未定义行为(参见条款37),但又不希望像 std::thread 那样直接终止程序,最终妥协设计了这种针对 std::async 任务的隐式 join 行为。
后果:开发者无法仅通过期值的类型来判断其析构函数是否会阻塞。这可能会在不经意间引入性能问题,尤其是在关键路径上或GUI线程中,意外的阻塞可能是致命的。
因此,在处理期值时,了解其来源至关重要。如果一个期值来自 std::async,你就必须警惕其析构函数可能导致的阻塞行为。