What is atomic type in C++?

Consider a multithreaded program with one shared variable. Now, imagine that all the threads are trying to access and modify this shared variable at the same time. When multiple threads try to access the same variable concurrently, and at least one of them is a write operation, then this is known as a data race. Some data races can cause race conditions which are a problem. Race condition is an error caused by the ordering of events such that the correctness of a program is compromised. Not all data races lead to race conditions and not all race conditions are caused by data races.

In order to solve this problem, C++ offers atomic variables that are thread-safe. They allow indivisible read, update and write operations on the shared variable without using mutexes (locks). Mutexes use software routines to lock and unlock operation/s, however, atomic variables utilise processor support through compare and swap instructions. In case of simple increments and decrements, atomic variables are significantly faster than locks.

How to use it

svg viewer

Code

In the code below, we have two variables: sum and multiThreadedSum. We increment sum 300,000 times using a single loop while we increment multiThreadedSum using 3 threads each of which iterates the increment loop 100,000 times. Ideally, the output in both the cases should be 300,000 but it is always less than that in the latter. Because the threads try to change the same memory location, they clash, resulting in loss of information. A variable is incremented by reading it’s value from a memory register, modifying it and then writing it back. When multiple threads try to update the same value, one thread might read an older value, increment and rewrite that back due to which the changes done by other threads, between the reading and writing of the first thread, is lost.

#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
// global variable
int multiThreadedSum=0;
// thread function
void sumNumbers(int increment){
for (int i=0; i<increment; i++){
multiThreadedSum++;
}
}
int main()
{
int sum = 0;
// Loop to increment 300000 times
for(int i=0; i<300000; i++){
sum++;
}
printf("The sum was %d\n",sum);
//-----------------
// initiating threads
thread th1(sumNumbers, 100000);
thread th2(sumNumbers, 100000);
thread th3(sumNumbers, 100000);
// waiting for the threads to end.
th1.join();
th2.join();
th3.join();
printf("The multi-threaded sum was %d\n",multiThreadedSum);
}

To solve this issue, we use atomic variable for the multi-threaded sum. They allow concurrent programming by making each operation indivisible. Hence, the above code can be rewritten as:

#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
// atomic variable
atomic<int> multiThreadedSum(0);
// thread function
void sumNumbers(int increment){
for (int i=0; i<increment; i++){
multiThreadedSum++;
}
}
//driver
int main()
{
int sum = 0;
// Loop to increment 300000 times
for(int i=0; i<300000; i++){
sum++;
}
printf("The sum was %d\n",sum);
//-----------------
// initiating threads
thread th1(sumNumbers, 100000);
thread th2(sumNumbers, 100000);
thread th3(sumNumbers, 100000);
// waiting for the threads to end.
th1.join();
th2.join();
th3.join();
printf("The multi-threaded sum was %d\n",multiThreadedSum.load());
}

Conclusion

To summarise, we use atomic variables to avoid information loss in parallel programming.

Copyright ©2024 Educative, Inc. All rights reserved