Will std::call_once
work properly for non-atomic variables? Consider the following code
std::once_flag once;
int x;
void init() { x = 10; }
void f() {
std::call_once(once, init);
assert(x == 10);
}
int main() {
std::thread t1(f), t2(f);
t1.join();
t2.join();
}
Will the side-effect in init
be seen to all threads when call_once
returns? Documentation on cppreference is kind of vague. It only says on all threads std::call_once
will return after init
is completed, but doesn't mention anything that prevents the x=10 being reordered after init
returns.
Any ideas? Where in the standard that clarifies the behavior?
The CPP Reference states std::call_once is thread safe: Executes the function f exactly once, even if called from several threads.
The std::call_once function, introduced in C++11, ensures a callable is called exactly one time, in a thread safe manner.
In order to solve this problem, C++ offers atomic variables that are thread-safe. The atomic type is implemented using mutex locks. If one thread acquires the mutex lock, then no other thread can acquire it until it is released by that particular thread.
It is unspecified whether any declaration in namespace std is available when <stdatomic.h> is included. The primary std::atomic template may be instantiated with any TriviallyCopyable type T satisfying both CopyConstructible and CopyAssignable.
You can use the function std::call_once to register a callable which will be executed exactly once. The flag std::call_once in the following implementation guarantees that the singleton will be thread-safe initialized. Here are the numbers. Of course, the most obvious way is it protects the singleton with a lock.
I'm totally aware of that. But the singleton pattern is an ideal use case for a variable, which has only to be initialized in a thread-safe way. From that point on you can use it without synchronization. So in this post, I discuss different ways to initialize a singleton in a multithreading environment.
When instantiated with one of the following integral types, std::atomic provides additional atomic operations appropriate to integral types such as fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor : The character types char, char8_t (since C++20), char16_t, char32_t, and wchar_t ;
Will the side-effect in init be seen to all threads when call_once returns?
Side effects from init
are visible to all threads that have called call_once
There is no more than one active execution (calling init
) but multiple passive executions are possible.
§ 30.4.6.2-2 - [thread.once.callonce]
An execution of call_once that does not call its func is a passive execution. An execution of call_once that calls its func is an active execution.
§ 30.4.6.2-3 - [thread.once.callonce]
Synchronization: For any given once_flag: all active executions occur in a total order; completion of an active execution synchronizes with (6.8.2) the start of the next one in this total order; and the returning execution synchronizes with the return from all passive executions.
So it is exactly as you expected
The main difference between atomic and non-atomic variables is that access to a non-atomic variable from multiple threads (unless all threads are reading) needs explicit synchronization to prevent the accesses from being potentially concurrent.
There are various ways to achieve this synchronization. The most common technique involves mutexes. The unlocking of a mutex by one thread synchronizes with the subsequent locking of that mutex by another thread. Thus, if the first thread writes a variable and the second thread reads that variable, an explicit ordering exists between the write and the read. The program then behaves as you expect: the read must see the last value written in that ordering. If mutexes were not used, the accesses to the variable would be potentially concurrent, and undefined behaviour would occur.
Atomic variables are self-synchronizing: no matter what, two threads attempting to access the same atomic variable will work out some order between them. Besides that, they don't have any special ability, compared to non-atomic variables, to be accessed by multiple threads.
The use of std::call_once
with the same flag by multiple threads sets up an explicit synchronization: each thread only returns from std::call_once
once init
has completed, so each thread must see the new value of x
.
The compiler is only allowed to reorder writes to the extent that it does not alter the observable behaviour of the program. Race conditions that you rationalize in terms of reordering disappear once you adhere to the standard by not allowing writes to a non-atomic variable to be potentially concurrent with another access to the same variable.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With