The disadvantage is that your code is just incorrect if you try to concurrently access a shared resource without the necessary protection (e.g. a mutex). Data races are undefined behavior, if you have it in your code, the compiler can in theory produce any nonsense imaginable. In practice, your code will could just crash catastrophically at runtime because some invariants no longer valid or memory gets corrupted, or it might just occasionally produce invalid results, it's impossible to tell.
You don't even need any special hardware to test this, try this on your computer, for example:
#include <mutex>
#include <string>
#include <thread>
#include <iostream>
std::string unprotected = "";
void task1() {
for (int i = 0; i < 100; ++i)
unprotected += '1';
}
void task2() {
for (int i = 0; i < 100; ++i)
unprotected += '2';
}
int main() {
std::thread t1{task1};
std::thread t2{task2};
t1.join();
t2.join();
std::cout << unprotected.length() << ": " << unprotected << std::endl;
}
The result contained anywhere between 100 and 200 characters, and for me it often just crashes with the message free(): invalid pointer
because the std::string
data is completely messed up because of the data races.
The solution in this case is to use a mutex to protect all concurrent accesses to the shared variable:
std::string unprotected = "";
std::mutex mtx;
void task1() {
for (int i = 0; i < 100; ++i) {
std::lock_guard<std::mutex> lck(mtx);
unprotected += '1';
}
}
void task2() {
for (int i = 0; i < 100; ++i) {
std::lock_guard<std::mutex> lck(mtx);
unprotected += '2';
}
}
Even though the order of the '1's and '2's in the string is still nondeterministic, you will now always get a string of length 200 and it will never crash.
Since bugs like can be very sporadic and hard to track down in some cases, tools like the Thread Sanitizer can help you at runtime, see https://godbolt.org/z/6qxM898z1.
For the first snippet above, it produces the following output:
WARNING: ThreadSanitizer: data race (pid=1)
Read of size 8 at 0x0000004052a8 by thread T2:
#0 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::size() const /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:920 (output.s+0x4026ef)
#1 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::push_back(char) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:1342 (output.s+0x4026ef)
#2 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(char) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:1179 (output.s+0x4026ef)
#3 task2() /app/example.cpp:18 (output.s+0x4026ef)
...
Previous write of size 8 at 0x0000004052a8 by thread T1:
#0 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_length(unsigned long) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:191 (output.s+0x4025b8)
#1 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_set_length(unsigned long) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:224 (output.s+0x4025b8)
#2 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::push_back(char) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:1346 (output.s+0x4025b8)
#3 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(char) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:1179 (output.s+0x4025b8)
#4 task1() /app/example.cpp:12 (output.s+0x4025b8)
...
Location is global 'unprotected[abi:cxx11]' of size 32 at 0x0000004052a0 (output.s+0x0000004052a8)
Thread T2 (tid=4, running) created by main thread at:
#0 pthread_create <null> (libtsan.so.0+0x5fdc5)
#1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdbb29)
#2 main /app/example.cpp:24 (output.s+0x40227d)
Thread T1 (tid=3, finished) created by main thread at:
#0 pthread_create <null> (libtsan.so.0+0x5fdc5)
#1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdbb29)
#2 main /app/example.cpp:23 (output.s+0x40226e)
SUMMARY: ThreadSanitizer: data race /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:920 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::size() const