Dealing with Sharing C++
If you don’t use Sharing C++, no data races can happen. Not sharing means that your thread works on local variables. This can be achieved by copying the value, using thread-specific storage, or transferring the result of a thread to its associated future via a protected data channel. The patterns in this section are quite obvious, but I will present them with a short explanation for completeness. Let me start with Copied Value. Copied Value Sharing C++ If a thread gets its arguments by copy and not by reference, there is no need to synchronize access to any data. No data races and no lifetime issues are possible. Data Races with References The following program creates three threads. One thread gets its argument by copy, the other by reference, and the last by constant reference. // copiedValueDataRace.cpp #include #include #include #include using namespace std::chrono_literals; void byCopy(bool b){ std::this_thread::sleep_for(1ms); // (1) std::cout << “byCopy: ” << b << ‘n’; } void byReference(bool& b){ std::this_thread::sleep_for(1ms); // (2) std::cout << “byReference: ” << b << ‘n’; } void byConstReference(const bool& b){ std::this_thread::sleep_for(1ms); // (3) std::cout << “byConstReference: ” << b << ‘n’; } int main(){ std::cout << std::boolalpha << ‘n’; bool shared{false}; std::thread t1(byCopy, shared); std::thread t2(byReference, std::ref(shared)); std::thread t3(byConstReference, std::cref(shared)); shared = true; t1.join(); t2.join(); t3.join(); std::cout << ‘n’; } Each thread sleeps for one millisecond (lines 1, 2, and 3) before displaying the boolean value. Only the thread t1 has a local copy of the boolean and has, therefore, no data race. The program’s output shows that the boolean values of threads t2 and t3 are modified without synchronization. You may think that the thread t3 in the previous example copiedValueDataRace.cpp can just be replaced with std::thread t3(byConstReference, shared). The program compiles and runs, but what seems like a reference is a copy. The reason is that the type traits function std::decay is applied to each thread argument. std::decay performs lvalue-to-rvalue, array-to-pointer, and function-to-pointer implicit conversions to its type T. In particular, it invokes, in this case, the type traits function std::remove_reference on the type T. The following program perConstReference.cpp uses a non-copyable type NonCopyableClass. // perConstReference.cpp #include class NonCopyableClass{ public: // the compiler generated default constructor NonCopyableClass() = default; // disallow copying NonCopyableClass& operator = (const NonCopyableClass&) = delete; NonCopyableClass (const NonCopyableClass&) = delete; }; void perConstReference(const NonCopyableClass& nonCopy){} int main(){ NonCopyableClass nonCopy; // (1) perConstReference(nonCopy); // (2) std::thread t(perConstReference, nonCopy); // (3) t.join(); } The object nonCopy (line 1) is not copyable. This is fine if I invoke the function perConstReference with the argument nonCopy (line 2) because the function accepts its argument per constant reference. Using the same function in the thread t (line 3) causes GCC to generate a verbose compiler error with more than 300 lines: The error message’s essential part is in the middle of the screenshot in red rounded rectangle: “error: use of deleted function”. The copy-constructor of the class NonCopyableClass is not available. When you borrow something, you have to ensure that the underlying value is still available when you use it. Lifetime Issues with References If a thread uses its argument by reference and you detach the thread, you have to be extremely careful. The small program copiedValueLifetimeIssues.cpp has undefined behavior. // copiedValueLifetimeIssues.cpp #include #include #include void executeTwoThreads(){ // (1) const std::string […]
