In C++, data race is a commonly occurring problem in concurrent programming. It occurs when two or more threads concurrently access the same memory location, and at least one of the accesses is a write. Data races lead to undefined behaviour, which means the program can exhibit unpredictable behavior, crash, or produce incorrect results. Preventing data races is crucial for writing safe and reliable concurrent programs.
In this article we will discuss what are data races, what are its causes and how we can prevent them from happening in our program.
What Causes Data Race in C++?
There are many possible causes of the data races on C++. It can occur due to the concurrent execution of threads without proper synchronization. In a multi-threaded environment, if threads do not coordinate their access to shared data, it can result in:
- Inconsistent Data: Different threads might read different values from the same variable.
- Corrupted Data: One thread might overwrite the data being used by another thread.
Example of Data Race in C++
The following program demonstrate the data race condition in a concurrent program:
C++
// C++ Program to illustrate the data race condition
#include <iostream>
#include <thread>
using namespace std;
// Global counter variable
int counter = 0;
// Function to increment the counter
void increment()
{
for (int i = 0; i < 100000; ++i) {
++counter;
}
}
int main()
{
// Create two threads that run the increment function
// concurrently
thread t1(increment);
thread t2(increment);
// Wait for both threads to finish
t1.join();
t2.join();
// Print the final counter value
cout << "Counter value: " << counter << endl;
return 0;
}
Output 1
Counter value: 146377
Output 2
Counter value: 200000
As seen from the above program, we may get different value in every execution. This is due to the data race condition.
How to Avoid Data Race in C++?
We can prevent the data races from happening by synchronizing the thread using various synchronizing primitives available in C++. Some of the are as follows:
- Mutex
- Atomic Operations
- Condition Variable
1. Preventing Data Race using Mutexes
A std::mutex can be used to ensure mutual exclusion, meaning that only one thread can access the critical section (shared data) of code at a time. Locking a mutex before accessing shared data ensures that only one thread can access the data at a time. We can unlock it when we are done working on it.
Syntax
std::mutex name // creating mutex
// In the callable
lock_guard<mutex> lock_name(name) // locking mutex
The mutex locked using lock_guard does not need to be explicitly unlocked at the end. They automatically gets unlocked when lock goes out of scope.
Example:
C++
// C++ Program to demonstrate how to use mutex for thread
// synchronization
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
// Define a mutex to protect the shared counter
mutex mtx;
int counter = 0;
// Function to increment the counter
void incrementCounter()
{
// locking mutex using lock_guard
lock_guard<mutex> lock(mtx);
for (int i = 0; i < 100000; ++i) {
++counter;
}
// mutex unlocked after going out of scope
}
int main()
{
// Create two threads that call incrementCounter
thread t1(incrementCounter);
thread t2(incrementCounter);
// Wait for both threads to finish
t1.join();
t2.join();
// Print the final counter value
cout << "Counter: " << counter << endl;
return 0;
}
Output
Counter: 200000
To know more about mutex in C++, refer to the article - Mutex in C++
2. Atomic Operations
In C++, std::atomic provides atomic operations on fundamental data types, which means these operations are performed as a single, indivisible step. Atomic operations prevent data races without the need for explicit locks.
Syntax
std::atomic <type> var_name;
Example
C++
// C++ program to illustrate how to aviod the data race
// using atomics
#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
// Define an atomic integer for the counter
atomic<int> counter(0);
// Function to increment the counter
void incrementCounter()
{
for (int i = 0; i < 100000; ++i) {
++counter;
}
}
int main()
{
// Create two threads that call incrementCounter
thread t1(incrementCounter);
thread t2(incrementCounter);
// Wait for both threads to finish
t1.join();
t2.join();
// Print the final counter value
cout << "Counter: " << counter << endl;
return 0;
}
Output
Counter: 200000
To know more about C++ Atomics, refer to the article - C++ 11 - <atomic> Header
3. Condition Variables
Condition variables are used to block a thread until a particular condition is met. They are used in conjunction with a mutex to wait for or signal changes in shared data.
Syntax
condition_variable name;
Example:
C++
// C++ Program to illustrate how to use condition variable
// to avoid the data race
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
mutex mtx;
// Condition variable for synchronization
condition_variable cv;
// Flag indicating whether work is ready
bool ready = false;
void wait_for_work()
{
unique_lock<mutex> lock(mtx);
// Wait until ready is true
cout << "Start Waiting" << endl;
cv.wait(lock, [] { return ready; });
// resuming the work here
cout << "Resuming the work after notification" << endl;
}
// Function to set ready flag
void set_ready()
{
lock_guard<mutex> lock(mtx);
// Mark work as ready
ready = true;
// Notify waiting threads
cout << "Notification Sent from the other thread"
<< endl;
cv.notify_one();
}
int main()
{
thread worker(wait_for_work);
thread producer(set_ready);
// Wait for the worker thread to finish
worker.join();
producer.join();
return 0;
}
Output
Start Waiting
Notification Sent from the other thread
Resuming the work after notification
To know more about condition variable, refer to the article - Condition Variables in C++ Multithreading
Related Articles:
Similar Reads
C++ Data Types
Data types specify the type of data that a variable can store. Whenever a variable is defined in C++, the compiler allocates some memory for that variable based on the data type with which it is declared as every data type requires a different amount of memory.C++ supports a wide variety of data typ
7 min read
Trie Data Structure in C++
A Trie, also known as a prefix tree, is a tree-like data structure used to store a dynamic set of strings. It is particularly useful for efficient retrieval of keys in a large dataset of strings. In this article, we will explore the Trie data structure, its operations, implementation in C++, and its
8 min read
Vector data() in C++ STL
In C++, the vector data() is a built-in function used to access the internal array used by the vector to store its elements. In this article, we will learn about vector data() in C++.Letâs take a look at an example that illustrates the vector data() method:C++#include <bits/stdc++.h> using nam
2 min read
Derived Data Types in C++
The data types that are derived from the primitive or built-in datatypes are referred to as Derived Data Types. They are generally the data types that are created from the primitive data types and provide some additional functionality.In C++, there are four different derived data types:Table of Cont
4 min read
std::string::data() in C++
The data() function writes the characters of the string into an array. It returns a pointer to the array, obtained from conversion of string to the array. Its Return type is not a valid C-string as no '\0' character gets appended at the end of array. Syntax: const char* data() const; char* is the po
2 min read
Data Layout in COBOL
COBOL is a programming language that was developed in the 1950s for business and financial applications. In COBOL, the data layout is the arrangement of data items in a program. It specifies how the data is organized and how it is accessed. COBOL programs are organized into four divisions: the iden
8 min read
std::distance in C++
The std::distance() in C++ STL is a built-in function used to calculate the number of elements between two iterators. It is defined inside <iterator> header file. In this article, we will learn about the std::distance function in C++ with examples.Example:C++// C++ Program to illustrate the us
5 min read
Structures in C++
C++ Structures are used to create user defined data types which are used to store group of items of different data types.SyntaxBefore using structure, we have to first define the structure using the struct keyword as shown:C++struct name{ type1 mem1; type2 mem2; ... };where structure name is name an
8 min read
Student Data Management in C++
Databases are being used in every aspect of our lives right now. Trillions of bytes of data are being stored in servers around the world. SQL is one of the most basic methods to use such a database. But have you ever thought about using C++ to maintain such a database. In this post, we will talk abo
8 min read
Data Types in Programming
In Programming, data type is an attribute associated with a piece of data that tells a computer system how to interpret its value. Understanding data types ensures that data is collected in the preferred format and that the value of each property is as expected. Data Types in Programming Table of Co
11 min read