之前内容的总结:
item37中说明了可结合的std::thread
对应于执行的系统线程。未延迟(non-deferred)任务的future(参见item36)与系统线程有相似的关系。
因此,可以将std::thread
对象和future对象都视作系统线程的句柄(handles)。
std::thread
和future在析构时有相当不同的行为。item37 说明,可结合的std::thread
析构会终止你的程序,因为两个其他的替代选择——隐式join
或者隐式detach
都是更加糟糕的。
在C++中,std::future和std::promise是用于处理异步操作结果的模板类。它们允许一个线程(调用者)等待另一个线程(被调用者)完成计算并获取其结果。这种机制背后的实现通过共享状态(shared state),是一个独立的对象,既不是std::promise也不是std::future,std::promise和std::future都引用了同一个共享状态,共享状态通常是基于堆的对象,但是标准并未指定其类型、接口和实现。标准库的作者可以通过任何他们喜欢的方式来实现共享状态。 当创建一个std::promise<T>对象时,可以通过调用它的get_future()成员函数来获得一个关联的std::future<T>对象。
std::future<T>对象可以用来等待或检查std::promise<T>是否已经设置了值,并最终获取那个值。
共享状态包含了以下信息:
(1)结果存储:实际的结果数据,即被调用者要传递给调用者的值或异常。
(2)同步原语:用于管理对结果的访问的锁或其他同步机制。
(3)状态标志:指示结果是否已经被设置、是否有错误发生等。
当被调用者完成了它的任务,它会调用std::promise的set_value()或者如果发生了异常则调用set_exception()方法,将结果写入共享状态。此时,任何持有与该std::promise相关联的std::future的线程都可以通过调用get()方法来检索结果,将阻塞直到结果可用。
( 目前觉得类似管道 )
由于std::promise通常是局部变量并且会在被调用者结束时销毁,std::future可能被复制成多个std::shared_future实例,因此结果不能直接存储在这些对象内部。相反,它们都指向堆上的共享状态,这个状态负责保存结果直到所有相关的std::future和std::shared_future都被销毁为止。这样的设计使得即使原始的std::promise和第一个std::future被销毁后,其他std::shared_future实例仍然能够访问结果,只要还有至少一个std::future或std::shared_future存在,共享状态就会保持有效,确保只可移动类型的结果可以被正确处理,因为移动操作不会影响共享状态中的实际数据。
future是通信信道的一端,被调用者通过该信道将结果发送给调用者。被调用者(通常是异步执行)将计算结果写入通信信道中(通常通过std::promise
对象),调用者使用future读取结果。
可以想象调用者,被调用者,共享状态之间关系如下图,虚线还是表示信息流方向:
共享状态非常重要,因为future的析构函数,取决于与future关联的共享状态。
在C++中,std::future 与 std::shared_future 类用于获取由异步操作产生的结果或异常。std::future 对象是独占的,而 std::shared_future 可以被多个对象共享。这些 future 对象关联着一个共享状态,这个状态包含未来将要得到的结果或异常。
(1)未延迟任务(即立即执行的任务)
当使用 std::async 启动一个任务时,默认情况下(如果没有指定 std::launch::deferred),任务会立即在一个独立线程中开始执行。返回的 std::future 对象引用了该任务的共享状态。如果这是最后一个引用该共享状态的 std::future 实例(例如,当它离开作用域并被销毁时),那么它的析构函数将会阻塞当前线程直到异步任务完成。这是因为最后一个 std::future 的销毁表示不再有其他地方等待这个异步操作的结果了,所以 C++ 标准库保证任务已经完成,并且结果或异常已经被处理。
(2)其他所有 std::future 的析构
如果有多个 std::future 或 std::shared_future 对象共享同一个状态,它们的销毁不会导致阻塞,除非它是最后一个引用该共享状态的对象。对于立即执行的任务,底层线程的行为类似于调用了 detach ,即使没有 std::future 对象再监视它,它也会继续运行。而对于延迟任务(通过 std::launch::deferred 指定),如果这是最后一个引用共享状态的 std::future 并且它被销毁了,那么该延迟任务永远不会被执行,因为它依赖于至少存在一个 std::future 来触发它的执行。
上面规则的解释:
真正要处理的是一个简单的“正常”行为以及一个单独的例外。正常行为是future析构函数销毁future。就是这样。意味着不join
也不detach
,也不运行什么,只销毁future的数据成员(当然,还做了另一件事,就是递减了共享状态中的引用计数,这个共享状态是由引用它的future和被调用者的std::promise
共同控制的。引用计数见item19)
正常行为的例外情况仅在某个future
同时满足下列所有情况下才会出现:
- 它关联到由于调用
std::async
而创建出的共享状态。 - 任务的启动策略是
std::launch::async(
参见item36),原因是运行时系统选择了该策略,或者在对std::async
的调用中指定了该策略。 - 这个future是关联共享状态的最后一个future。
对于std::future
,情况总是如此,对于std::shared_future
,如果还有其他的std::shared_future
,与要被销毁的future引用相同的共享状态,则要被销毁的future遵循正常行为(简单地销毁它的数据成员)。只有当上面的三个条件都满足时,future的析构函数才会表现“异常”行为,就是在异步任务执行完之前阻塞住。实际上,这相当于对由于运行std::async
创建出任务的线程隐式join
。
正常行为
对于大多数情况下的 std::future 或 std::shared_future 的析构,行为是相当直接和简单的:
(1)销毁数据成员:当一个 std::future 被销毁时,会简单地清理自己的资源,包括它的内部数据成员。
(2)递减引用计数:更重要的是,它会对共享状态中的引用计数进行递减操作。这个引用计数用于追踪有多少个 std::future 或 std::shared_future 正在引用同一个共享状态。这是管理资源生命周期的一种方式,确保只有当没有其他 future 对象引用该状态时,相关资源才能被安全地释放。
异常行为(特殊条件)
异常行为仅在特定条件下发生,即当以下所有条件同时满足时:
(1)关联到由 std::async 创建的共享状态:意味该 future 是通过调用 std::async 来创建的,而 std::async 通常用于启动异步任务。
(2)任务使用 std::launch::async 策略:这里指的是异步任务确实是在一个独立的线程中执行的。如果策略是 std::launch::deferred,那么任务会在第一次访问结果时才被执行,并且不会立即开始。
(3)最后一个引用此共享状态的 future:如果这是一个与共享状态关联的最后一个 std::future 或 std::shared_future,则意味着不再有其他对象等待这个异步操作的结果。
异常行为的表现
当上述三个条件都满足时,std::future 的析构函数将表现出“异常”行为。它不会简单地销毁自己并递减引用计数,而是会阻塞当前线程,直到关联的异步任务完成。这实际上相当于对启动该任务的线程进行了隐式的 join 操作。这种设计确保了即使没有任何显式代码在等待结果,也能保证异步任务正确完成,防止潜在的资源泄漏或未定义行为。
关于 std::future 析构时的行为不确定性
问题核心:无法通过 std::future 的 API 确定它是否引用了由 std::async 创建的共享状态,因此不能提前知道析构函数是否会因为等待异步任务完成而阻塞。
std::vector<std::future<void>> futs;
// 可能在析构时阻塞,如果其中至少一个 future 引用了 std::async 启动的任务。
解决方案:当使用 std::packaged_task 创建共享状态时,可以明确知道该 std::future 不会因 std::async 而产生阻塞行为,因为它关联的是 std::packaged_task 创建的共享状态。
int calcValue(); //函数定义
//包裹函数以异步运行
std::packaged_task<int()> pt(calcValue);
auto fut = pt.get_future(); //获取 future
std::thread 和 std::packaged_task 的结合使用
线程管理:当将 std::packaged_task 传递给 std::thread 执行时,需要确保正确地处理线程的生命周期(join 或 detach),这通常会在代码中显式地进行,从而避免 std::future 的析构函数阻塞。
{
std::packaged_task<int()> pt(calcValue);
auto fut = pt.get_future();
// 将 task 移交给 thread 执行
std::thread t(std::move(pt));
// 此处可以对 t 进行 join 或 detach 操作
// 如果不做任何操作,t 在作用域结束时会导致程序终止
// 示例:
// t.join(); // 显式 join
// 或者
// t.detach(); // 显式 detach
} // 结束代码块
当有一个与 std::packaged_task 创建的共享状态相关联的 std::future 时,不需要担心其析构函数会执行异常行为,因为通常会在代码中做出关于线程生命周期的决定(例如,调用 join 或 detach)。此外,如果要使用 std::async 来运行任务,就没有必要再额外使用 std::packaged_task,因为 std::async 已经涵盖了 std::packaged_task 的所有功能。
请记住:
- future的正常析构行为就是销毁future本身的数据成员。
- 引用了共享状态,使用
std::async
启动的未延迟任务建立的最后一个future的析构函数会阻塞住,直到任务完成。