C++11保证在函数的第一次调用时静态局部变量的初始化是原子的。尽管该标准没有强制执行任何实现,但有效处理此问题的唯一方法是双重检查锁定。
我问自己,是否所有对象都初始化了,是否在同一个互斥对象之间初始化(可能),或者每个静态对象初始化是否对自己的互斥对象起作用(不太可能)。因此,我编写了这个litlte ++20-程序,它使用一些可变的和折叠的表达式技巧来拥有许多不同的函数,每个函数都初始化自己的静态对象:
#include <iostream>
#include <utility>
#include <latch>
#include <atomic>
#include <chrono>
#include <thread>
using namespace std;
using namespace chrono;
atomic_uint globalAtomic;
struct non_trivial_t
{
non_trivial_t() { ::globalAtomic = ~::globalAtomic; }
non_trivial_t( non_trivial_t const & ) {}
~non_trivial_t() { ::globalAtomic = ~::globalAtomic; }
};
int main()
{
auto createNThreads = []<size_t ... Indices>( index_sequence<Indices ...> ) -> double
{
constexpr size_t N = sizeof ...(Indices);
latch latRun( N );
atomic_uint synch( N );
atomic_int64_t nsSum( 0 );
auto theThread = [&]<size_t I>( integral_constant<size_t, I> )
{
latRun.arrive_and_wait();
if( synch.fetch_sub( 1, memory_order_relaxed ) > 1 )
while( synch.load( memory_order_relaxed ) );
auto start = high_resolution_clock::now();
static non_trivial_t nonTrivial;
nsSum.fetch_add( duration_cast<nanoseconds>( high_resolution_clock::now() - start ).count(), memory_order_relaxed );
};
(jthread( theThread, integral_constant<size_t, Indices>() ), ...);
return (double)nsSum / N;
};
constexpr unsigned N_THREADS = 64;
cout << createNThreads( make_index_sequence<N_THREADS>() ) << endl;
}
我用上面的代码创建了64个线程,因为我的系统在处理器组中有多达64个CPU(RyzenThreadrapper3990X,Windows 11)。这些结果满足了我的期望,每一次初始化都要花费大约7000 is。如果每个初始化都对它自己的互斥锁起作用,则互斥锁将采用较短的路径,并且没有内核争用,并且时间会更低。还有什么问题吗?
之后我问自己的问题是:如果静态对象的构造函数有自己的静态对象,会发生什么?标准是否明确要求这应该工作,从而迫使实现考虑互斥必须是递归的?
发布于 2022-05-15 03:15:00
不,静态初始化并不是所有对象的原子化。不同的静态对象可以由不同的线程同时初始化。
GCC和Clang实际上确实使用了一个全局递归互斥(用于处理您描述的递归情况,这是工作所必需的),但其他编译器对每个静态函数(本地对象(即Apple的编译器)使用互斥)。因此,您不能依赖一次一个对象的静态初始化--仅仅是因为它没有,这取决于编译器(以及编译器的版本)。
标准的6.7.4部分:
具有静态存储持续时间的POD类型(basic.types)的本地对象在第一次输入其块之前被初始化。实现允许在名称空间范围(basic.start.init)中静态初始化具有静态存储持续时间的对象的相同条件下,对具有静态存储持续时间的其他本地对象执行早期初始化。 否则,这样的对象将在第一次通过其声明时被初始化;这样的对象在初始化完成后被认为是初始化的。如果通过抛出异常退出初始化,则初始化不完成,因此下次控件输入声明时将再次尝试初始化。如果控件在初始化对象时(递归地)重新输入声明,则行为未定义.。
标准只禁止对同一个静态对象进行递归初始化;它不禁止对一个静态对象进行初始化以要求对另一个静态对象进行初始化。由于标准明确规定,在第一次执行包含这些对象的块时,必须初始化不属于此禁用类别的所有静态对象,因此允许使用您询问的情况。
int getInt1();
int getInt2() { //This could be a constructor, too, and nothing would change
static int result = getInt1();
return result;
}
int getInt3() {
static int result = getInt2(); //Allowed!
return result;
}
这也适用于函数的构造函数本地静态对象本身包含这样一个静态对象的情况。构造函数实际上也只是一个函数,这意味着这种情况与上面的示例完全相同。
发布于 2022-05-15 06:23:18
每个静态局部变量都必须是原子的。如果它们中的每一个都有自己的互斥锁或双重检查锁,那么这将是正确的。
还可以有一个全局递归互斥体,允许一个线程和一个线程一次只初始化静态局部变量。这也很管用。但是,如果您有许多静态局部变量和多个线程第一次访问它们,那么这可能会非常慢。
但是,让我们考虑静态局部变量具有静态局部变量的情况:
class A {
static int x = foo();
};
void bla() {
static A a;
};
初始化a
需要初始化x
。但是,没有什么可以说,不可能有其他线程也有一个A c;
,并将初始化x
在同一时间。因此,x
仍然需要受到保护,即使在bla()
的情况下,它是在已经静态的初始化中。
另一个示例(希望编译,尚未检查):
void foo() {
static auto fn = []() {
static int x = bla();
};
}
在这里,x
只能在初始化fn
时才能初始化。所以编译器可能会跳过保护x
。这将是一个最佳化,它遵循“如果是主体”。除了定时之外,x
是否受到保护也没有区别。另一方面,x
的锁定总是成功的,而且成本非常低。编译器可能不会对其进行优化,因为没有人投入时间来检测和优化这种情况。
https://stackoverflow.com/questions/72247395
复制