In multi-thread programming, data races are a common problem in threads. Data races occur when multiple tasks or threads access a shared resource without sufficient protection, leading to undefined or unpredictable behavior on runtime. In C++, the concurrency support library is designed to solve these kinds of data race problems. In this post, we explain how to solve data race problems in modern C++.
Table of Contents
What is data race in multi-threading C++ apps?
We use multi-threading code when we want to handle multiple tasks to speed up functions or algorithms. Multithreaded programming is easy with the concurrency support library in C++. However, if you don’t know how to reach each data type, multi-thread operations can be highly complex and introduce subtle defects such as data races and deadlocks. At this time, defects occur on runtime or at outputs, this may take a long time to reproduce the issue and even longer to identify the root cause and fix the defect.
How can we solve data race problems with modern C++?
If you have a problem in your multi-threading application and you understand that is about data racing in threads. First, if your app is popular and you want to fix and release it quickly, set it to a single thread (slower but faster and safer and gives you time to solve the problem). Thus, your application may run, with slower performance, but no defects during runtime.
Now we can focus on our real problem. First, know that the data race problem is about accessing your data in usage in your threads. Try to find which thread is causing this, and what type of data could be having a situation that at least one operation in threads trying to write. Debugging in thread operations may help you and try to slow your thread by using sleep_for
in threads.
1 2 3 |
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); |
If your debugger doesn’t help, you can ‘comment’ some parts of data write operations. Inside a thread, between the suspicious lines, you can add some printouts into a variable (to string lists for example) to see each operation step in debug before the error occurs.
In deep, you can use every feature of the concurrency support library in C++, we highly recommend you C++17 or higher compilers to achieve better results in usage. Use std::thread
, and std::atomic
with atomic types and operations (load
, store
), and you can use memory order (std::memory_order
) models in different situations. Moreover, you can use fences (std::atomic_thread_fence
), mutex (std::mutex
) and other blocking or locking methods in threads.
The concurrency support library is designed to solve these kinds of data race problems. This is your exact solution and there might be different ways to solve with different concurrency features in modern C++. Note that, there might be slight differences in operational speed and thread usage, so you should decide which feature is the best for your thread function or functions. They may help your algorithm to speed up and to be safer too.
Solving multi-thread problems may require high programming skills, if you are still unable to solve problems, you may get additional support from senior C++ developers or supporter developers of your IDE and compiler.
Is there a data race example in modern C++?
Assume that we have a computer shop and we have items in the store. We move them from shop to store and from store to shop. We have many staff (threads) that transfer these items from store to shop or shop to store. Different staff may access to store or shop at the same time. In this simple data race example, the same thread function ( transfer_items()
) reads and writes two different variables (store_items
and shop_items
) where they show the number of items in store and shop. Here is a full example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> #include <thread> int store_items = 20; int shop_items = 0; //------------------------------------------------------------------ void transfer_items(int amount, int& out, int& in) { out -= amount; std::this_thread::sleep_for(std::chrono::nanoseconds(10)); in += amount; } //------------------------------------------------------------------ int main() { std::cout << store_items << " + " << shop_items << std::endl; std::thread t1( transfer_items, 5, std::ref(store_items), std::ref(shop_items) ); std::thread t2( transfer_items, 2, std::ref(shop_items), std::ref(store_items) ); t1.join(); t2.join(); std::cout << store_items << " + " << shop_items << std::endl; system("pause"); } |
How we can solve this data race example in modern C++?
As you see, our threads are reading and writing to the same data (&in and &out) where they are store_items and shop_items in global variables. We added 10 ns waiting that may cause problems on different read and write operations of different tasks. To solve this problem, we can use std::atomic
variables for these int
types and we can do add and subtract operations on them. We can solve this by using atomic variables as below,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <iostream> #include <thread> #include <atomic> std::atomic<int> store_items = 20; std::atomic<int> shop_items = 0; //------------------------------------------------------------------ void transfer_items( std::atomic<int>& out, std::atomic<int>& in, int amount ) { out -= amount; std::this_thread::sleep_for(std::chrono::nanoseconds(10)); in += amount; } //------------------------------------------------------------------ int main() { std::cout << store_items << " + " << shop_items << std::endl; std::thread t1( transfer_items, std::ref(store_items), std::ref(shop_items), 5 ); std::thread t2( transfer_items, std::ref(shop_items), std::ref(store_items), 2 ); t1.join(); t2.join(); std::cout << store_items << " + " << shop_items << std::endl; system("pause"); } |
Note that, your problem may require different features of concurrency support library. You can find many useful modern and simple multi-thread examples in our LearnCPlusPlus.org.
C++ Builder is the easiest and fastest C and C++ IDE for building simple or professional applications on the Windows, MacOS, iOS & Android operating systems. It is also easy for beginners to learn with its wide range of samples, tutorials, help files, and LSP support for code. RAD Studio’s C++ Builder version comes with the award-winning VCL framework for high-performance native Windows apps and the powerful FireMonkey (FMX) framework for cross-platform UIs.
There is a free C++ Builder Community Edition for students, beginners, and startups; it can be downloaded from here. For professional developers, there are Professional, Architect, or Enterprise version.