Open In App

Data Races in C++

Last Updated : 19 Jun, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

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:

  1. Mutex
  2. Atomic Operations
  3. 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:



Next Article
Practice Tags :

Similar Reads