concurrent-io · part 03

static std::mutex is safe to construct, thanks to magic statics

· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak

Before C++20 there was no osyncstream, and the fix for a shared sink was the one you already reach for: a mutex. Guard every write with it and the threads take turns.

void log_line(...) {
    static std::mutex mtx;
    const std::lock_guard lock(mtx);
    std::cout << ...;
}

The static local is the interesting part. It is initialized the first time control passes through the declaration, which under threads raises a question: what if two threads reach it at once? Do they both construct the mutex, or worse, use it half-built?

Neither. The counter in the demo, incremented in a function-local static’s constructor, reads exactly one after three threads stampede the function. That is the C++11 guarantee for function-local statics, the feature people call “magic statics”: if control enters the declaration while another thread is initializing the variable, the second thread waits for the first to finish. Exactly one initialization, and everyone who arrives during it blocks until it is done.

What it cost to get here

This was undefined before C++11, and the workaround was double-checked locking, famously easy to get subtly wrong. GCC and Clang implemented the guarantee early; MSVC only shipped it in Visual Studio 2015, behind /Zc:threadSafeInit. After the first call the steady-state cost is a single relaxed load of a guard flag, a couple of nanoseconds, so the lazy static singleton is finally both correct and cheap.

Mutex or osyncstream?

They solve the same problem differently. The mutex serializes the whole write: while one thread holds it, the others wait, and nothing formats concurrently. osyncstream lets threads format in parallel into private buffers and serializes only the final emit. The mutex is simpler and needs no <syncstream>; the syncstream keeps more work off the critical section. For a log handler that formats a line and writes it, either is correct. The mutex has one advantage the syncstream lacks: it does not need the whole program to cooperate.


Sources: [dcl.init] (function-local static init) in the C++ working draft · C++ Stories: thread safety of function-local statics · MSVC /Zc:threadSafeInit.