Home C++ Decisions Loops Input/Output Functions Stack and Heap References Arrays Searching and Sorting Recursion Pointers Character and Strings Structures Classes Inheritance Exceptions Templatess STL Modern C++ Misc Books ----

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: 491617
We 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: 1000000
The "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 program

File: 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