std::call_once 与 std::async

The Old New Thing (Raymond Chen) 工具

摘要

本文比较了 C++ 中用于延迟执行的 std::call_once 和 std::async,讨论了它们在实现、性能和异常行为方面的差异。

<p>上次,我们比较了<a title="魔法静态变量与 std::call_once" href="https://devblogs.microsoft.com/oldnewthing/20260916-00/?p=112703">魔法静态变量与 <code>std::call_once</code></a>,并得出结论:<code>std::call_once</code> 允许你为非静态变量构建类似魔法静态变量的行为。</p> <p>但还有用于延迟执行的 <code>std::async</code>。我们能否用它来替代呢?</p> <p>这里的思想是,你告诉 <code>std::async</code> 你想要延迟执行某个东西(比如一个 lambda)。它返回一个代表该延迟执行的 <code>std::future</code>。</p> <pre>auto f = std::async(std::launch::deferred, ⟦ lambda ⟧); </pre> <p>在之后的某个时刻,你可以要求执行延迟执行并检索结果。</p> <pre>auto value = future.get(); </pre> <p>这里有几个陷阱。</p> <p>为了允许获取不可复制的类型,获取值是一个破坏性操作:你只能调用一次 <code>get()</code>,后续调用会导致未定义行为。对于需要多次请求值的情况,这是一个问题,但你可以通过将 <code>std::<wbr />future</code> 转换为 <code>std::<wbr />shared_<wbr />future</code> 来修复:</p> <pre>auto f = std::async(std::launch::deferred, ⟦ lambda ⟧)<span style="border: solid 1px currentcolor;">.share()</span>; </pre> <p>当你调用 <code>shared_<wbr />future</code> 的 <code>get()</code> 时,它会给你一个指向缓存值的 const 引用,并保留缓存值供未来调用。<code>shared_<wbr />future::<wbr />get()</code> 方法被标记为 <code>const</code>,在 C++ 标准库中,这意味着它相对于自身和其他 <code>const</code> 成员是线程安全的。因此,你可以随心所欲地多次调用 <code>get()</code>,第一次将运行 lambda 并返回结果,后续将返回已计算的结果。</p> <p>好的,所以我们的 <code>Gadget</code> 类可以这样:</p> <pre>class Gadget { public: Gadget(std::shared_ptr&lt;Widget&gt; const&amp; widget) : widget(widget) {} bool can_reverse_polarity() { return can_reverse_polarity_future.get(); } private: std::shared_ptr&lt;Widget&gt; const widget; std::shared_future&lt;bool&gt; const can_reverse_polarity_future = std::async(std::launch::deferred, [=] { return is_configuration_enabled("polarity_reversal") &amp;&amp; is_widget_polarity_reversible(*widget); }).share(); }; </pre> <p>那么为什么选择其中一个而不是另一个呢?</p> <p>嗯,<code>std::call_<wbr />once</code> 非常小。Visual Studio 基于 Win32 的 <code>INIT_ONCE</code> 构建它,其大小为一个指针。¹</p> <p>另一方面,<code>std::<wbr />future</code> 和 <code>std::<wbr />shared_<wbr />future</code> 涉及堆分配以管理共享状态,以及存储可调用对象及其参数和结果。此外,由于 <code>std::<wbr />async</code> 支持其他执行模式,你可能会引入支持那些模式的代码。(例如,它必须考虑你传递 <code>std::<wbr />launch::<wbr />async</code> 的可能性,因此它会链接线程库以及支持 <code>wait_<wbr />for</code> 的其他机制。)</p> <p>但它们之间的一个重要区别在于它们的异常行为,我们甚至还没有讨论过。</p> <p>我们将在下次讨论。</p> <p>¹ 我找不到 gcc 是基于什么构建它的,但我发现的一个旧实现只是<a href="https://classpages.cselabs.umn.edu/current/csci5103/tools/installs/gcc-4.8.3+os161-2.1/libgcc/config/i386/gthr-win32.c">手动构建</a>,有许多缺陷。快速查看一下,它既不是异常安全的,也存在数据竞争。代码似乎已经<a href="https://github.com/gcc-mirror/gcc/blob/9149a5b7e0a66b7b94d5b7db3194a975d18dea2f/libgcc/config/i386/gthr-win32.h#L629">移动过</a>,但它仍然存在。似乎<a href="https://github.com/gcc-mirror/gcc/blob/9f6eb7d2993046e5720275f590098db0ac0188d4/libstdc%2B%2B-v3/include/std/mutex#L922">缺乏异常安全被用一个类似 TODO 的注释指出了</a>。数据竞争通过一个注释来解决,该注释说处理器隐式地使所有加载成为 acquire,所有存储成为 release,虽然这可能正确,但它不能阻止<i>编译器</i>重新排序存储和加载。编译器可能会决定内联回调,然后重新排序存储,使得对 <code>done</code> 的存储在回调结束之前发生。</p> <p>文章 <a href="https://devblogs.microsoft.com/oldnewthing/20260917-00/?p=112706">&lt;CODE&gt;std::call_once&lt;/CODE&gt; 对 &lt;CODE&gt;std::async&lt;/CODE&gt;</a> 最初发表在 <a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a> 上。</p>
查看原文
查看缓存全文

缓存时间: 2026/09/18 02:23

# std::call\_once 与 std::async - 新旧事物纵横谈 来源:https://devblogs.microsoft.com/oldnewthing/20260917-00?p=112706 上次我们将**魔法静态变量**与 `std::call_once`(https://devblogs.microsoft.com/oldnewthing/20260916-00/?p=112703)进行了比较,得出结论:`std::call_once` 能够为非静态变量实现类似魔法静态变量的行为。不过,`std::async` 也提供了延迟执行的能力。我们能否用它来替代呢? 核心思路是:通过 `std::async` 指定延迟执行某段逻辑(比如一个 Lambda 表达式),它会返回一个表示该延迟任务的 `std::future` 对象。 ```cpp auto f = std::async(std::launch::deferred, lambda); ``` 在后续任意时刻,你可以请求执行这个延迟任务并获取结果: ```cpp auto value = future.get(); ``` 但这里有几个需要注意的点。为了支持获取不可拷贝类型,`get()` 是**破坏性操作**:只能调用一次,多次调用会导致未定义行为。如果你需要多次获取结果,可以将 `std::future` 转换为 `std::shared_future` 来解决: ```cpp auto f = std::async(std::launch::deferred, lambda).share(); ``` 对 `shared_future` 调用 `get()` 会返回缓存值的常量引用,并保留该值供后续调用使用。`shared_future::get()` 方法被标记为 `const`,根据 C++ 标准库约定,这意味着它相对于自身和其他 `const` 成员是线程安全的。因此你可以任意次调用 `get()`:首次调用将执行 Lambda 并返回结果,后续调用直接返回已缓存的结果。 这样我们的 `Gadget` 类可以这样实现: ```cpp class Gadget { public: Gadget(std::shared_ptr<Widget> const& widget) : widget(widget) {} bool can_reverse_polarity() { return can_reverse_polarity_future.get(); } private: std::shared_ptr<Widget> const widget; std::shared_future<bool> const can_reverse_polarity_future = std::async(std::launch::deferred, [=] { return is_configuration_enabled("polarity_reversal") && is_widget_polarity_reversible(*widget); }).share(); }; ``` 那么为何要在两者之间选择其一呢? `std::call_once` 体积非常小。在 Visual Studio 中,它基于 Win32 的 `INIT_ONCE` 实现,大小仅相当于一个指针。而 `std::future` 和 `std::shared_future` 需要堆内存分配来管理共享状态、存储可调用对象及其参数和结果。此外,由于 `std::async` 支持多种执行模式,即使你只使用延迟执行,也会引入支持其他模式的代码(例如它需要考虑你传递 `std::launch::async` 的可能性,因此会链接线程库以及支持 `wait_for` 的其他机制)。 但两者更关键的差异在于异常处理机制——我们尚未讨论这一点。这将在下文展开。 --- **注释** 1. 我未能查明 GCC 的具体实现方式,但早期实现采用手动编写的方式,存在诸多缺陷。粗略浏览可发现它既不具备异常安全性,也存在数据竞争问题。相关代码虽经过重构但核心问题依然存在。其中异常安全性的缺失在类似 TODO 的注释中被指出,而数据竞争则通过一条说明“处理器隐式保证所有加载具有 acquire 语义、所有存储具有 release 语义”的注释来解释。虽然这在处理器层面可能成立,但无法阻止**编译器**对存储和加载操作进行重排序。编译器可能会内联回调函数,并重排存储指令,使得对 `done` 的存储发生在回调结束之前。 ### 分类 话题 ### 作者 Raymond Chen Raymond 参与 Windows 演进超过30年。2003年他创立了名为《新旧事物纵横谈》的网站,其受欢迎程度远超他最疯狂的想象——这种发展仍让他感到不安。该网站衍生出同名书籍(Addison Wesley 2007)。他偶尔会出现在 Windows 开发文档 Twitter 账号上,讲述一些毫无实用价值的故事。

相似文章

魔法静态变量 vs. std::call_once

The Old New Thing (Raymond Chen)

本文比较了两种 C++ 技术:魔法静态变量和 std::call_once,用于缓存计算,并解释了根据作用域和初始化等因素何时使用哪种技术。

C++ futures 第三部分:std::async 的心智模型

The Old New Thing (Raymond Chen)

Raymond Chen 解释 C++ std::async 的心智模型,说明 launch 策略的两个维度(立即/延迟、同线程/新线程)中哪些组合是标准允许的,哪些被禁止或由实现决定。

D2300R11: `std::execution`

Lobsters Hottest

本文提出了一个标准的C++框架,用于管理异步执行,引入了调度器、发送器和接收器,以增强C++编程中的异步性和并行性。