线程安全延迟初始化:静态 vs std::call_once vs 双重检查锁定

问题描述 投票:0回答:1

对于线程安全的延迟初始化,应该更喜欢函数内的静态变量、std::call_once 还是显式双重检查锁定?有什么有意义的差异吗?

这三个都可以在这个问题中看到。

C++11 中的双重检查锁单例

Google 中出现了 C++11 中双重检查锁定的两个版本。

Anthony Williams 展示 双重检查锁定与显式内存排序和 std::call_once。他没有提到 static,但那篇文章可能是在 C++11 编译器可用之前写的。

Jeff Preshing 在一篇广泛的writeup 中描述了双重检查锁定的几种变体。他确实提到使用静态变量作为选项,甚至表明编译器将生成双重检查锁定的代码来初始化静态变量。我不清楚他是否得出结论,一种方法比另一种方法更好。

我觉得这两篇文章都是为了教学,没有理由这样做。如果您使用静态变量或 std::call_once,编译器将为您完成此操作。

c++ multithreading c++11 double-checked-locking
1个回答
41
投票

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 保护足以执行此获取/释放模式,因此它可以在执行双重锁定时对第一个字节进行raw读取,而不是获取-读取。 这是可能的,因为 GCC 不使用 C++ 原子 API 来执行双重锁定——它使用的是特定于平台的方法

GCC 在一般情况下无法优化原子。 在设计为不太同步的架构上(例如为 1024 个以上内核设计的架构),GCC 无法依赖架构来执行 LL/SS。因此,GCC 被迫实际发出原子。 然而,在 x86 和 x64 等常见平台上,它可以更快。

call_once
可以具有 GCC 静态的效率,因为它类似地将可以对
once_flag
执行的操作数量限制为可以应用于原子的函数的一小部分。 代价是静态在适用时使用起来要方便得多,但是
call_once
在静态不足的许多情况下都有效(例如动态生成的对象拥有的
once_flag
)。

在这些更高的平台上,静态和

call_once
之间的性能略有差异。 其中许多平台虽然不提供 LL/SS,但至少会提供整数的非撕裂读取。 这些平台可以使用它和线程特定的指针来进行每个线程纪元计数以避免原子。 这对于静态或
call_once
来说已经足够了,但取决于计数器是否翻转。 如果您没有无撕裂的 64 位整数,则
call_once
必须担心翻转。 实现可能会也可能不会担心这一点。 如果它忽略这个问题,它可以像静态一样快。 如果它关注这个问题,它必须像原子一样慢。 Static 在编译时知道有多少静态变量/块,因此它可以证明编译时没有翻转(或者至少有信心!)

© www.soinside.com 2019 - 2024. All rights reserved.