线程安全惰性初始化:静态vs std::call_once vs双重检查锁定
Threadsafe lazy initialization: static vs std::call_once vs double checked locking
对于线程安全延迟初始化,应该更喜欢函数内部的静态变量std::call_once还是显式双重检查锁定?有什么有意义的区别吗?
这三者都可以从这个问题中看出。
C++11 中的双重检查锁单
谷歌上出现了两个版本的C++11中的双重检查锁定。
Anthony Williams展示了带有显式内存排序和std::call_once的双重检查锁定。他没有提到static,但这篇文章可能是在C++11编译器问世之前写的。
Jeff Preshing在一篇详尽的文章中描述了双重检查锁定的几种变体。他确实提到使用静态变量作为选项,他甚至展示了编译器将生成用于双重检查锁定的代码来初始化静态变量。我不清楚他是否得出这样的结论:一种方法比另一种更好。
我觉得这两篇文章都是为了教学,没有理由这么做。如果您使用静态变量或std::call_once,编译器将为您执行此操作。
GCC使用特定于平台的技巧来完全避免快速路径上的原子操作,利用它可以比call_once或双重检查更好地分析static
这一事实。
因为双重检查使用原子论作为避免种族情况的方法,所以每次都要付出获取的代价。这不是一个高价格,但这是一个价格。
它必须为此付出代价,因为原子在任何情况下都必须保持原子性,即使是比较交换这样的困难操作。这使得优化变得非常困难。一般来说,编译器必须将其保留在中,以防您使用该变量而不仅仅是双重锁。没有简单的方法可以证明你从未在原子上使用过更复杂的操作。
另一方面,static
是高度专业化的,也是语言的一部分。它从一开始就被设计成非常容易进行可证明的初始化。因此,编译器可以使用更通用的版本无法使用的快捷方式。编译器实际上为静态发出以下代码:
一个简单的功能:
void foo() {
static X x;
}
在GCC内部重写为:
void foo() {
static X x;
static guard x_is_initialized;
if ( __cxa_guard_acquire(x_is_initialized) ) {
X::X();
x_is_initialized = true;
__cxa_guard_release(x_is_initialized);
}
}
它看起来很像一把双重检查的锁。然而,编译器在这里有点作弊。它知道用户永远不能直接使用cxa_guard
进行编写。它知道它只在编译器选择使用它的特殊情况下使用。因此,有了这些额外的信息,它可以节省一些时间。尽管CXA保护规范是分布式的,但它们都有一个共同的规则:__cxa_guard_acquire
永远不会修改保护的第一个字节,而__cxa_guard__release
会将其设置为非零。
这意味着每个保护必须是单调的,并且它确切地指定了将要执行的操作。因此,它可以利用主机平台内现有的种族情况保护。例如,在x86上,强同步CPU保证的LL/SS保护足以实现这种获取/释放模式,因此当它执行双重锁定时,它可以对第一个字节进行原始读取,而不是获取读取。这是可能的,因为GCC没有使用C++原子API来进行双重锁定,而是使用特定于平台的方法。
GCC在一般情况下无法优化原子。在设计为不太同步的架构(例如为1024+核设计的架构)上,GCC不需要依赖于原始架构来为其进行LL/SS。因此,GCC被迫实际发射原子。然而,在x86和x64等常见平台上,它可以更快。
call_once
可以具有GCC静态的效率,因为它类似地将可以对once_flag
执行的操作数量限制为可以应用于原子的函数的一小部分。权衡的结果是,静态在适用时使用起来要方便得多,但call_once
在静态不足的许多情况下都能工作(例如动态生成的对象拥有的once_flag
)。
在这些更高的平台上,static和call_once
的性能略有不同。这些平台中的许多虽然不提供LL/SS,但至少会提供整数的非撕裂读取。这些平台可以使用它和特定于线程的指针来进行每个线程的历元计数,以避免原子化。这对于静态或call_once
来说就足够了,但取决于计数器是否翻转。如果您没有一个可撕裂的64位整数,call_once
必须担心翻转。实现可能担心也可能不担心这一点。如果它忽略了这个问题,它可以像静态一样快。如果它关注这个问题,就必须像原子论一样缓慢。静态在编译时知道有多少静态变量/块,所以它可以证明在编译时没有滚动(或者至少非常自信!)
- 在VS代码中交叉编译Windows与Linux上的MinGW的SDL程序
- 如何为模板化对象创建模板向量?VS正在投掷C3203
- 数据成员SFINAE的C++17测试:gcc vs clang
- 为什么在Windows上的VS 2019和Clang 9中"size_t"在没有标题的情况下工作
- 在for循环中使用auto vs decltype(vec.size())来处理字符串的向量
- 正在VS调试器中监视映射条目
- Confusion: decltype vs std::function
- 将IBM Rhapsody模型集成到VS 2019中
- VS Code "command":"make"与终端窗口中的命令行"make"不同
- 使用VS Code和CMake Tools运行自定义命令
- 修改 VS Code 中的默认C++代码段
- 如何使用c++在VS 2019上运行SQL查询
- vs 2015 constexpr变量不恒定,但与2019相比还好吗
- 完美前进使用 std::forward vs RefRefCast
- 从VS 2015更新3更新到VS2015更新3 d后浮点计算行为不同的原因
- VS 2015 链接错误 无法构建依赖于 libcurl 的项目
- consteval wrapper vs. source_location
- VS Code C++:不准确的系统包括路径错误(wchar.h,boost/lambda/lambda.hpp)
- QStringList vs list<shared_ptr<QString>> 性能比较C++
- VS 2017 使用交叉编译器构建 x64 项目