Why Every C++ Developer Should Understand Atomic Variables
For a C++ developer, ensuring safe data access across multiple threads is critical. While mutexes offer a traditional solution, atomic variables provide a lightweight, high-performance alternative. By enabling safe reads and writes without heavy locking, atomic variables allow you to synchronize operations and enforce proper ordering efficiently. Let's look at how they prevent data races and how to implement them.
When multiple threads need access to the same data for update, there is a need to synchronize the access to the data. To do that without using atomics, mutexes are needed. By the help of mutexes, the right thread will access the data to read or write at the right moment. That can be done using the mutex together with a lock_guard.
When the lock_guard object is created, it takes ownership of the mutex. When it goes out of scope, the lock_guard is destructed and the mutex is released so another thread can access the data. Using atomic variables, reading and writing can be done without this heavy synchronization. For a C++ developer, atomics are essential since they can be used to synchronize operations and enforce ordering.
Thread Safety and Data Races
Atomic variables are visible across threads and main, but atomic operations themselves are invisible. Other threads can observe the atomic variable before the operation and after the operation, but not the operation itself. It means that reading and writing without additional synchronization is allowed.
If many threads increment a variable without atomicity, the increment is not thread-safe. That is so because the compiler first loads the value from memory into a register, increments it, and then stores it back in the memory. Another thread might access the memory during the operation and increment the same variable, which causes a data race. That does not happen to atomic variables.
When to Use Atomic Variables in Multi-Threading
Atomic variables are also very useful when you share data between multiple threads. Every C++ developer should know that for smaller data types, atomics can improve efficiency. It is a great tool when:
Several threads update or increment to the same data or the same variable.
Operations need to be synchronized without the overhead of mutexes.
A thread needs to signal to another thread that an operation is carried out and the data is ready to use for that particular thread.
An example of that is demonstrated in the code below. All calculations are carried out using non-atomic variables. In the example, there are two threads. In one of the threads, a calculation is carried out, and in the other thread, the result is printed on the screen inside the thread. Then the result is printed in main. The threads must be synchronized to make it work. The order must be enforced, and that can be done using atomic variables.
Code Example: Implementing Atomic Variables in C++
The Main Function
On the first line in main, the two integers are initialized. And then an object is created and the two integers are passed as arguments. Then the two threads are initialized. As can be seen, the address of the object is supplied as an object pointer. The two last lines of code are functions printing the result of the calculation.
Cpp File Breakdown
On the first line, the constructor can be found where the two integers are initialized.
Function One
The first function will be executed in the first thread. It prints on the screen that it is thread one. There is no inherent guarantee about the order in which parts of your code in different threads will run. Therefore it is written. Then there is a while loop. The loop will run as long as the calculation from thread two is not carried out. The order is enforced.
In the next line, the answer to the calculation is printed. The answer is then stored in an atomic variable using the atomic function store(). Store replaces the current value of the atomic variable with a new value. The value is stored in an atomic variable because the atomic variable can then be printed in main. The last line is an atomic bool variable that is set to true. That is a message to the function print_Res.
Function Two
The first line in the second function prints the message that it is the second function. Then the calculation is carried out. When that is done, the atomic bool variable Calc_ready is set to true. That is a message to the first function.
Function Three
In function three, there is a while loop that runs until the calculation is ready. The while loop stops running when the atomic bool variable in function two is set to ready. Then the calculation is printed. In this function, the atomic::load() function is used. Load retrieves the current value without any modification.
Function Four
In the fourth function, the atomic variable answer is printed without waiting for the calculation to be finished. This function prints the atomic integer without ordering. It means that it can print the private data member initialized to zero or the result of the calculation. It is most likely that it will print 0.
As a C++ developer, it is meant as a reminder that one has to be careful when dealing with atomic variables. Make sure that the code does what it is supposed to do.
Cpp file
Header File Structure
In the header file, the constructor can be found and also the two functions that will be executed in the threads. The two last functions will print the result in main. They are all declared public.
Then there are three integers declared private. The two first are initialized in the constructor and the third will get the result from the calculation. Then there are two atomic bool variables declared false and an atomic int variable.
Header file
Key Takeaways for the C++ Developer
The result is printed twice in main. In the function print_Res_no_order, there is no enforced order to make sure the answer is printed when the calculation is carried out. The atomic variable answer will, most likely, be printed with the initialized value in private in the header file and not the calculated value.
That is taken care of in the print_Res function with the while loop. The order is enforced in that function. That is not the case in function print_Res_no_order. As pointed out previously, this is a warning to be careful when dealing with atomic variables. Every C++ developer must make sure that the concurrent code using atomic variables does what it is supposed to do.