account

Race Conditions versus Data Races

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. That is very bad. 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 software domain.

  • 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 when at least two threads access a shared variable simultaneously. At least one thread tries to modify the variable.

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

Before I present you with 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 of 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.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    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 first removed from the old account and then added to the new one. Two money transfers take place (3). One from account1 to account2, and the other way around. Each invocation of transferMoney happens after the other. They are a kind of transaction 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, must be moved or copied by value. If a reference such as account1 or account2 needs to be passed to the thread function, you must wrap it in a reference wrapper like std::ref. Because of the threads t1 and t2, there is a data race on the account’s balance 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 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 of the new built-in literals. We have had them for time durations since C++14.

    By the way. A short sleep period in concurrent programs is often 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 withdrawal happened before the first money transfer was completed. Here we have our race condition.

    Solving the data race is relatively easy. The operations on 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 also have deadlocks without race conditions. In the next post, I write about the malicious effects of race conditions.

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, and Philipp Lenk.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *