std::call_once 与 std::async
摘要
本文比较了 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<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();
};
</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"><CODE>std::call_once</CODE> 对 <CODE>std::async</CODE></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 账号上,讲述一些毫无实用价值的故事。
相似文章
比较 magic statics、std::call_once 和 std::async 的异常行为
本文比较了 C++ 并发机制中的异常处理行为:magic statics、std::call_once 和 std::async,解释了它们的差异并提供了使用建议。
魔法静态变量 vs. std::call_once
本文比较了两种 C++ 技术:魔法静态变量和 std::call_once,用于缓存计算,并解释了根据作用域和初始化等因素何时使用哪种技术。
C++ future 第 4 部分:如果我想要一个立即在当前线程上执行的 std::async,该怎么办?
Raymond Chen 探讨了在 C++ 中创建立即在当前线程上执行的 std::future 的变通方法,包括使用 std::promise、std::packaged_task,或使用延迟执行的 std::async 并立即等待其完成。
C++ futures 第三部分:std::async 的心智模型
Raymond Chen 解释 C++ std::async 的心智模型,说明 launch 策略的两个维度(立即/延迟、同线程/新线程)中哪些组合是标准允许的,哪些被禁止或由实现决定。
D2300R11: `std::execution`
本文提出了一个标准的C++框架,用于管理异步执行,引入了调度器、发送器和接收器,以增强C++编程中的异步性和并行性。