Threads
Contents
Atomicity
We know that if 2 threads try to access a shared variable like an integer we need to protect that shared variable. We have the "atomic" class that does this for us.File: atomic1.cpp
#include <iostream> #include <thread> #include <atomic> #include <vector> using namespace std ; // An atomic counter atomic<int> atomic_counter(0); // A non-atomic counter for comparison int non_atomic_counter = 0; void increment_atomic_counter() { for (int i1 = 0; i1 < 100000; ++i1) { atomic_counter++; // Atomic increment } } void increment_non_atomic_counter() { for (int i1 = 0; i1 < 100000; ++i1) { non_atomic_counter++; // Non-atomic increment } } int main() { vector<thread> threads; // Test with atomic counter cout << "Testing atomic counter..." << endl; for (int i = 0; i < 10; ++i) { threads.push_back(thread(increment_atomic_counter)); } for (auto& t : threads) { t.join(); } cout << "Atomic counter final value: " << atomic_counter << endl; // Expected: 10 * 100000 = 1000000 // Reset and test with non-atomic counter threads.clear(); cout << "\nTesting non-atomic counter..." << endl; for (int i = 0; i < 10; ++i) { threads.push_back(thread(increment_non_atomic_counter)); } for (auto& t : threads) { t.join(); } cout << "Non-atomic counter final value: " << non_atomic_counter << endl; // Likely less than 1000000 due to race conditions return 0; } $ g++ atomic1.cpp ; ./a.exe Testing atomic counter... Atomic counter final value: 1000000 Testing non-atomic counter... Non-atomic counter final value: 491617We could have protected the non-atomic integer ourselves as shown in the following program but the "atomic" keyword save us that extra work.
File: atomic2.cpp
#include <iostream> #include <thread> #include <mutex> #include <atomic> #include <vector> using namespace std ; // An atomic counter atomic<int> atomic_counter(0); // A non-atomic counter for comparison int non_atomic_counter = 0; void increment_atomic_counter() { for (int i1 = 0; i1 < 100000; ++i1) { atomic_counter++; // Atomic increment } } mutex mutexObj ; void increment_non_atomic_counter() { for (int i1 = 0; i1 < 100000; ++i1) { mutexObj.lock() ; non_atomic_counter++; // Non-atomic increment mutexObj.unlock() ; } } int main() { vector<thread> threads; // Test with atomic counter cout << "Testing atomic counter..." << endl; for (int i = 0; i < 10; ++i) { threads.push_back(thread(increment_atomic_counter)); } for (auto& t : threads) { t.join(); } cout << "Atomic counter final value: " << atomic_counter << endl; // Expected: 10 * 100000 = 1000000 // Reset and test with non-atomic counter threads.clear(); cout << "\nTesting non-atomic counter..." << endl; for (int i = 0; i < 10; ++i) { threads.push_back(thread(increment_non_atomic_counter)); } for (auto& t : threads) { t.join(); } cout << "Non-atomic counter final value: " << non_atomic_counter << endl; // Likely less than 1000000 due to race conditions return 0; } $ g++ atomic2.cpp ; ./a.exe Testing atomic counter... Atomic counter final value: 1000000 Testing non-atomic counter... Non-atomic counter final value: 1000000The "atomic" class is usually used with primitive types but can be used with classes. However the class should be simple and not have any copy constructor, assignment operators, destructors. We can use a construct called comapre-and-swap type function to update the custom object value. The "comapre-and-swap" can take 2 arguments; a current value of what is stored and the new value. If the current value matches with what's in the memory then it is updated with the new value.
File: atomic_class1.cpp
#include <iostream> #include <atomic> #include <thread> // For demonstration with multiple threads // A custom class that is trivially copyable struct MyData { int x1; double y1; // Default constructor MyData(int val_x = 0, double val_y = 0.0) : x1(val_x), y1(val_y) {} // No user-defined special member functions (copy/move constructors/assignments, destructor) // No virtual functions or virtual base classes }; // Overload operator== for MyData for use with compare_exchange_strong bool operator==(const MyData& lhs, const MyData& rhs) { return lhs.x1 == rhs.x1 && lhs.y1 == rhs.y1; } void worker_function(atomic<MyData>& atomic_data) { MyData old_data; MyData new_data; // Perform a compare-and-swap loop to update the atomic MyData do { old_data = atomic_data.load(); // Atomically load the current value new_data = old_data; new_data.x1++; // Modify the copied data new_data.y1 += 0.1; } while (!atomic_data.compare_exchange_strong(old_data, new_data)); // Atomically attempt to update //If the compare_exchange operation failed then we try again. //else the old_data was replaced with new_data } int main() { atomic<MyData> shared_data(MyData(10, 5.5)); // Initialize with a MyData object //load loads the current value cout << "Initial Data: x=" << shared_data.load().x << ", y=" << shared_data.load().y << endl; //worker_function needs a reference thread t1(worker_function, ref(shared_data)); thread t2(worker_function, ref(shared_data)); t1.join(); t2.join(); cout << "Final Data: x=" << shared_data.load().x1 << ", y=" << shared_data.load().y1 << endl; // You can also use store() and load() directly shared_data.store(MyData(20, 10.0)); cout << "Stored new Data: x=" << shared_data.load().x << ", y=" << shared_data.load().y << endl; return 0; }
Exercise
1) Find the issue in the below programFile: atomic_ex1.cpp
#include <iostream> #include <thread> #include <mutex> #include <atomic> #include <vector> using namespace std ; // An atomic counter atomic<int> atomic_counter(0); // A non-atomic counter for comparison int non_atomic_counter = 0; void increment_atomic_counter() { for (int i1 = 0; i1 < 100000; ++i1) { atomic_counter++; // Atomic increment } } void increment_non_atomic_counter() { mutex mutexObj ; for (int i1 = 0; i1 < 100000; ++i1) { mutexObj.lock() ; non_atomic_counter++; // Non-atomic increment mutexObj.unlock() ; } } int main() { vector<thread> threads; // Test with atomic counter cout << "Testing atomic counter..." << endl; for (int i = 0; i < 10; ++i) { threads.push_back(thread(increment_atomic_counter)); } for (auto& t : threads) { t.join(); } cout << "Atomic counter final value: " << atomic_counter << endl; // Expected: 10 * 100000 = 1000000 // Reset and test with non-atomic counter threads.clear(); cout << "\nTesting non-atomic counter..." << endl; for (int i = 0; i < 10; ++i) { threads.push_back(thread(increment_non_atomic_counter)); } for (auto& t : threads) { t.join(); } cout << "Non-atomic counter final value: " << non_atomic_counter << endl; // Likely less than 1000000 due to race conditions return 0; }2) Find the issue in the below program
File: atomic_ex2.cpp
#include <iostream> #include <thread> #include <atomic> #include <vector> using namespace std ; // An atomic counter atomic<int> atomic_counter(0); void increment_atomic_counter() { for (int i1 = 0; i1 < 100000; ++i1) { int temp1 = atomic_counter ; temp1 = temp1 + 1 ; atomic_counter = temp1 ; } } int main() { vector<thread> threads; // Test with atomic counter cout << "Testing atomic counter..." << endl; for (int i = 0; i < 10; ++i) { threads.push_back(thread(increment_atomic_counter)); } for (auto& t : threads) { t.join(); } cout << "Atomic counter final value: " << atomic_counter << endl; // Expected: 10 * 100000 = 1000000 // Reset and test with non-atomic counter return 0; }
Solutions
1)File: atomic2.cpp