Is a mutex defined statically in a function body able to lock properly?
Asked Answered
G

2

7

Is a mutex defined statically in a function body able to lock properly? I am currently using this pattern in my logger system, but I have not tested it's thread safety yet.

void foo () {
    static std::mutex mu;
    std::lock_guard<std::mutex> guard(mu);
    ...
}
Greenock answered 9/11, 2018 at 22:13 Comment(0)
P
1

NathanOliver's answer is not accurate: mu is actually statically initialized – this means before any dynamic initialization, and therefore also before any user code could call mu.lock() (whether directly or by using std::lock_guard<std::mutex>).

Nonetheless, your use case is safe – in fact, std::mutex initialization is even more safe than suggested by the previous answer.

The reason for this is that any variable with static storage duration (✓ check) that is initialized with a constant expression (where a call to a constexpr constructor is explicitly considered as such – ✓ check), is constant initialized, which is a subset of static initialized. All static initialization happens strictly before all dynamic initialization, and hence before your function can be called the first time. (basic.start.static/2)

This applies to std::mutex because std::mutex has only one viable constructor, the default constructor, and it is specified to be constexpr. (thread.mutex.class)

Therefore, in addition to the usual atomicity guarantee that C++11 and higher makes for dynamic initialization of static variables at function scope, other std::mutex instances with static storage are also completely unaffected by initialization order issues, e.g.:

 #include <mutex>

 extern std::mutex mtx;
 unsigned counter = 0u;
 const auto count = []{ std::lock_guard<std::mutex> lock{mtx}; return ++counter; };
 const auto x = count(), y = count();
 std::mutex mtx;

If mtx was dynamically initialized, this code would exhibit undefined behavior, because, then, mtx's initializer would run after that of the dynamically initialized x and y, and mtx would therefore be used before it is initialized.

(In pthread, or common implementations of <thread> that use pthread, this effect is achieved by the use of the constant expression PTHREAD_MUTEX_INITIALIZER.)

PS: This is also true for instances of std::atomic<T>, as long as the argument passed to the constructor is a constant expression. This means e.g. that you can easily make a spin lock based on std::atomic<IntT> that is immune to initialization order issues. std::once_flag has the same desirable property. A std::atomic_flag with static storage duration can also be statically initialized in either of two ways:

  • std::atomic_flag f;, is zero-initialized (because of static storage duration and because it has a trivial default c'tor). Note that the state of the flag is nonetheless unspecified, which makes this approach rather useless.

  • std::atomic_flag f = ATOMIC_FLAG_INIT; is constant initialized and unset. This is what you'd actually want to use.

Pentateuch answered 10/11, 2018 at 14:34 Comment(0)
G
10

Yes, this is fine. The first time the function is called mu will be initialized (and this is guaranteed to be thread safe and only happen once) and then guard will lock it. If another thread calls foo it will wait at

std::lock_guard<std::mutex> guard(mu);

until the first call to foo completes and guard is destroyed unlocking mu.

Geochronology answered 9/11, 2018 at 22:16 Comment(0)
P
1

NathanOliver's answer is not accurate: mu is actually statically initialized – this means before any dynamic initialization, and therefore also before any user code could call mu.lock() (whether directly or by using std::lock_guard<std::mutex>).

Nonetheless, your use case is safe – in fact, std::mutex initialization is even more safe than suggested by the previous answer.

The reason for this is that any variable with static storage duration (✓ check) that is initialized with a constant expression (where a call to a constexpr constructor is explicitly considered as such – ✓ check), is constant initialized, which is a subset of static initialized. All static initialization happens strictly before all dynamic initialization, and hence before your function can be called the first time. (basic.start.static/2)

This applies to std::mutex because std::mutex has only one viable constructor, the default constructor, and it is specified to be constexpr. (thread.mutex.class)

Therefore, in addition to the usual atomicity guarantee that C++11 and higher makes for dynamic initialization of static variables at function scope, other std::mutex instances with static storage are also completely unaffected by initialization order issues, e.g.:

 #include <mutex>

 extern std::mutex mtx;
 unsigned counter = 0u;
 const auto count = []{ std::lock_guard<std::mutex> lock{mtx}; return ++counter; };
 const auto x = count(), y = count();
 std::mutex mtx;

If mtx was dynamically initialized, this code would exhibit undefined behavior, because, then, mtx's initializer would run after that of the dynamically initialized x and y, and mtx would therefore be used before it is initialized.

(In pthread, or common implementations of <thread> that use pthread, this effect is achieved by the use of the constant expression PTHREAD_MUTEX_INITIALIZER.)

PS: This is also true for instances of std::atomic<T>, as long as the argument passed to the constructor is a constant expression. This means e.g. that you can easily make a spin lock based on std::atomic<IntT> that is immune to initialization order issues. std::once_flag has the same desirable property. A std::atomic_flag with static storage duration can also be statically initialized in either of two ways:

  • std::atomic_flag f;, is zero-initialized (because of static storage duration and because it has a trivial default c'tor). Note that the state of the flag is nonetheless unspecified, which makes this approach rather useless.

  • std::atomic_flag f = ATOMIC_FLAG_INIT; is constant initialized and unset. This is what you'd actually want to use.

Pentateuch answered 10/11, 2018 at 14:34 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.