Why thread synchronization is required in C++ | View Program behavior

Hi, in this article I will show you why thread synchronization is required in C++ program.  You will understand the concepts clearly by observing the behaviour of a program WITHOUT and WITH thread synchronization.

[Intent of this article is to understand the effect of thread synchronization, not about each statement of the program.]

In short:

Why thread synchronization is required in C++?

Thread synchronization is required in C++ program to prevent undefined behaviour, including crashes or unexpected results when multiple threads are using shared resources (file, memory, data structure, global variable etc.).

[Shared resource means, multiple threads are accessing the same resource.]

You will understand the concepts clearly in a moment.

Result without thread synchronization,

here’s a simple C++ application that demonstrates creating two threads, one for adding elements to a vector and another for reading and printing elements from the vector: (Here the vector is being shared between 2 threads to add elements, and to retrieve and print elements respectively)

RUN THIS CODE

in your C++ IDE. Try running multiple times, and you’ll find undefined behaviour, crashes or unexpected results, or no outputs etc.

#include <iostream>
#include <vector>
#include <thread>

// Global vector to store integers
std::vector<int> numbers;

// Function for the first thread to add elements to the vector
void addElements() {
    for (int i = 1; i <= 50; ++i) {
        numbers.push_back(i);
    }
}

// Function for the second thread to read and print elements from the vector
void printElements() {
    for (int i = 0; i < numbers.size(); ++i) {
        std::cout << "Element " << i << ": " << numbers[i] << std::endl;
    }
}

int main() {
    std::thread t1(addElements); // First thread to add elements
    std::thread t2(printElements); // Second thread to print elements

    t1.join(); // Wait for the first thread to finish
    t2.join(); // Wait for the second thread to finish

    return 0;
}

Reason to get undefined behaviour without thread synchronization is:

Since there’s no guarantee on the order of execution of threads, the printElements thread might start accessing elements of the vector before the addElements thread has finished adding elements. This can lead to the printElements thread accessing elements that are not yet added or partially added, resulting in undefined behavior.

Result with thread synchronization,

Now let’s write the above same code with Thread Synchronization where you will see the result correctly.

(To synchronize both threads, I will be using mutex and lock_guard)

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>

// Global vector to store integers
std::vector<int> numbers;
std::mutex mtx; // Mutex for thread synchronization

// Function for the first thread to add elements to the vector
void addElements() {
    for (int i = 1; i <= 50; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // Lock the mutex
        numbers.push_back(i);        
    }
}

// Function for the second thread to read and print elements from the vector
void printElements() {
    for (int i = 0; i < numbers.size(); ++i) {
        std::lock_guard<std::mutex> lock(mtx); // Lock the mutex
        std::cout << "Element " << i << ": " << numbers[i] << std::endl;
    }
}

int main() {
    std::thread t1(addElements); // First thread to add elements
    std::thread t2(printElements); // Second thread to print elements

    t1.join(); // Wait for the first thread to finish
    t2.join(); // Wait for the second thread to finish

    return 0;
}

CONCLUSION:

In summary, without proper synchronization mechanisms like mutexes, locks, or atomic operations, concurrent access to shared data can result in unpredictable behavior, crashes, or data corruption.

It’s essential to use synchronization to ensure thread safety and avoid these issues.