Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::call_once safe for non atomic variables?

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?

like image 846
Kan Li Avatar asked Jan 12 '18 07:01

Kan Li


People also ask

Is std :: Call_once thread safe?

The CPP Reference states std::call_once is thread safe: Executes the function f exactly once, even if called from several threads.

What is std :: Call_once?

The std::call_once function, introduced in C++11, ensures a callable is called exactly one time, in a thread safe manner.

Are atomic variables thread safe C++?

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.

Is it possible to declare stdatomic in namespace std?

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.

How to make a singleton callable thread safe?

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.

Is it possible to initialize a singleton variable without synchronization?

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.

What is the use of atomic in C++?

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 ;


2 Answers

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

like image 144
LWimsey Avatar answered Nov 08 '22 07:11

LWimsey


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.

like image 32
Brian Bi Avatar answered Nov 08 '22 08:11

Brian Bi