Race Conditions versus Data Races

Contents[Show]

Race conditions and data races are related but different concepts. Because they are related, they are often confused. In German we even translate both expressions with the term kritischer Wettlauf. To be honest, that is very bad. In order to reason about concurrency, your wording must be exact. Therefore, this post is about race conditions and data races.

 

At a starting point, let me define both terms in the domain of software.

  • Race condition: A race condition is a situation, in which the result of an operation depends on the interleaving of certain individual operations.
  • Data race: A data race is a situation, in which at least two threads access a shared variable at the same time. At least on thread tries to modify the variable.

A race condition is per se not bad. A race condition can be the reason for a data race. In contrary, a data race is an undefined behaviour. Therefore, all reasoning about your program makes no sense anymore.

Before I present you different kinds of race conditions that are not benign, I want to show you a program with a race condition and a data race.

A race condition and a data race

The classic example for a race condition and a data race is a function that transfers money from one account to another. In the single threaded case, all is fine.

Single threaded

 

// account.cpp

#include <iostream>

struct Account{                                  // 1
  int balance{100};
};

void transferMoney(int amount, Account& from, Account& to){
  if (from.balance >= amount){                  // 2
    from.balance -= amount;                    
    to.balance += amount;
  }
}

int main(){
  
  std::cout << std::endl;

  Account account1;
  Account account2;

  transferMoney(50, account1, account2);         // 3
  transferMoney(130, account2, account1);
  
  std::cout << "account1.balance: " << account1.balance << std::endl;
  std::cout << "account2.balance: " << account2.balance << std::endl;
  
  std::cout << std::endl;

}

 

The workflow is quite simple to make my point clear. Each Account starts with a balance of 100 $ (1). To withdraw money, there must be enough money in the account (2). If enough money is available the amount will be at first removed from the old account and then added to the new. Two money transfers take place (3). One from account1 to account2, and the other the other way around. Each invocation of transferMoney happens after the other. They are kind of transactions that establishes a total order. That is fine.

The balance of both accounts looks good.

account

 In real life, transferMoney will be executed concurrently.

Multithreading

 No, we have a data race and a race condition.

 

// accountThread.cpp

#include <functional>
#include <iostream>
#include <thread>

struct Account{
  int balance{100};
};
                                                      // 2
void transferMoney(int amount, Account& from, Account& to){
  using namespace std::chrono_literals;
  if (from.balance >= amount){
    from.balance -= amount;  
    std::this_thread::sleep_for(1ns);                 // 3
    to.balance += amount;
  }
}

int main(){
  
  std::cout << std::endl;

  Account account1;
  Account account2;
                                                        // 1
  std::thread thr1(transferMoney, 50, std::ref(account1), std::ref(account2));
  std::thread thr2(transferMoney, 130, std::ref(account2), std::ref(account1));
  
  thr1.join();
  thr2.join();

  std::cout << "account1.balance: " << account1.balance << std::endl;
  std::cout << "account2.balance: " << account2.balance << std::endl;
  
  std::cout << std::endl;

}

 

The calls of transferMoney will be executed concurrently (1). The arguments to a function, executed by a thread, have to be moved or copied by value. If a reference such as account1 or account2 needs to be passed to the thread function, you have to wrap it in a reference wrapper like std::ref. Because of the threads t1 and t2, there is a data race on the balance of the account in the function transferMoney (2). But where is the race condition? To make the race condition visible, I put the threads for a short period of time to sleep (3). The built-in literal 1ns in the expression std::this_thread::sleep_for(1ns) stands for a nanosecond. In the post Raw and Cooked are the details to the new built-in literals. We have them for time durations since C++14.

By the way. Often a short sleep period in concurrent programs is sufficient to make an issue visible.

Here is the output of the program.

accountThreads

And you see. Only the first function transferMoney was executed. The second one was not performed because the balance was too small. The reason is that the second withdraw happened before the first transfer of money was completed. Here we have our race condition.

Solving the data race is quite easy. The operations on the balance have to be protected. I did it with an atomic variable.

// accountThreadAtomic.cpp

#include <atomic>
#include <functional>
#include <iostream>
#include <thread>

struct Account{
  std::atomic<int> balance{100};
};

void transferMoney(int amount, Account& from, Account& to){
  using namespace std::chrono_literals;
  if (from.balance >= amount){
    from.balance -= amount;  
    std::this_thread::sleep_for(1ns);
    to.balance += amount;
  }
}

int main(){
  
  std::cout << std::endl;

  Account account1;
  Account account2;
  
  std::thread thr1(transferMoney, 50, std::ref(account1), std::ref(account2));
  std::thread thr2(transferMoney, 130, std::ref(account2), std::ref(account1));
  
  thr1.join();
  thr2.join();

  std::cout << "account1.balance: " << account1.balance << std::endl;
  std::cout << "account2.balance: " << account2.balance << std::endl;
  
  std::cout << std::endl;

}

 

Of course, the atomic variable will not solve the race condition. Only the data race is gone.

What's next?

I only presented an erroneous program having a data race and a race condition. But there are many different aspects of malicious race conditions. Breaking of invariants, locking issues such as deadlock or livelocks, or lifetime issues of detached threads. We have also deadlocks without race conditions. In the next post, I write about malicious effects of race conditions.

 

 Thanks a lot to my Patreon Supporter: Eric Pederson.

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Get your e-book. Support my blog.

 

Comments   

+1 #1 Gavin 2018-03-27 09:42
Keep this going please, grwat job!
Quote
+1 #2 Declan 2018-03-31 08:05
Hi! I just want to give you a big thumbs
up for the great info you have right here on this post.
I am returning to your web site for more soon.
Quote
0 #3 Matthew Wycliff 2018-11-24 01:06
Mr. Rainer,

Thank you for taking the time to explain this intimidating subject. You did a great job of breaking it down so that the rest of us can get a clear understanding of multithreading and the pitfalls of data races and race conditions. You da man!
Quote

Add comment


Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 1562

All 1407750

Currently are 174 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments