C++11 guarantees that the initialization of static local variables is atomic at the first call of the function. Although the standard doesn't mandate any implementation, the only way to handle this efficiently is double-checked locking.
I asked myself if all objects are initialized are initialized across the same mutex (likely) or if each static object initialization acts on its own mutex (unlikely). So I wrote this litlte C++20-program that uses some variadic and fold expression tricks to have a number of different functions that each initialize their own static object:
#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;
}
I create 64 threads with the above code since my system has up to 64 CPUs in a processor group (Ryzen Threadripper 3990X, Windows 11). The results fulfilled my expectations in a way that each initialization is reported to take about 7.000ns. If each initialization would act on its own mutex the mutex locks would take the short path and you'd have no kernel-contention and the times would be magnitudes lower. So are there any further questions ?
The question I asked myself afterwards is: what happens if the constructor of the static object has its own static object ? Does the standard explicitly mandate that this should work, forcing the implementation to consider that the mutex has to be recursive ?