Multithreading | Handling Race Conditions and Deadlocks in C++
Tags: programming, tutorial, cpp, performance
Published: 2023-02-17 15:54:54

Previously, we've seen some different ways to create a thread instance and how to manage them using the `join()` and `detach()` methods.

In this article, I will discuss about move semantics of thread ownership, mutexes, and other related topics.

## Move Ownership of a Thread:
In C++ `<thread>` by using `std::move` function we can move the ownership of an executable thread between different thread instances/objects.

As `std::thread` is a resource owning type, a thread of execution can only be moved across different instances but not copied to eliminate the chance of resource wastage.

Okay, enough talking! let's have a look at the example code,

```
1. void my_work();  //callable function
2. void my_other_work();
3. std::thread my_worker1(my_work);  //construction of thread 
   //object specifying the task
4. std::thread my_worker2;  //default constructed thread object
5. my_worker2 = std::move(my_worker1);  //moving ownership of the 
   //executable thread
6. my_worker1 = std::thread(my_other_work);  //construction of 
   //a new thread associated with temporary object
```

_`Code Example:`_
```C++
#include <iostream>
#include <thread>

int count = 0;
void my_work() {
    while (count++ < 2) {
        std::cout << "Doing my work!" << "\n";
    }
    std::cout << "My work is complete!" << "\n";
}
void my_other_work() {
    while (count++ < 2) {
        std::cout << "Doing my other work!" << "\n";
    }
    std::cout << "My other work is complete!" << "\n";
}

int main() {
    std::thread my_worker1(my_work);
    std::cout << "Thread Id of my_worker1 is " << my_worker1.get_id() << "\n";
    std::thread my_worker2;
    my_worker2 = std::move(my_worker1);
    my_worker1 = std::thread(my_other_work);
    std::cout << "Thread Id of my_worker1 is " << my_worker1.get_id() << "\n";
    std::cout << "Thread Id of my_worker2 is " << my_worker2.get_id() << "\n";
    my_worker1.join();
    my_worker2.join();
    return 0;
}
```

_`Code Output:`_
```
Thread Id of my_worker1 is 2
Thread Id of my_worker1 is 3
Thread Id of my_worker2 is 2
Doing my work!
My work is complete!
Doing my other work!
My other work is complete!
```
_`Explanation:`_

In our above code, at first in `main` function we created a new thread `my_worker1` using the `std::thread` constructor, which is associated with the `my_work` function.

Next, we created a default constructed thread object `my_worker2`(i.e. it is not associated with any executable thread). In which we moved the `my_worker1` thread object, using `std::move` function.

Doing this transfers the ownership of the underlying thread from `my_worker1` to `my_worker2`, leaving `my_worker1` in an invalid state.

After this, we have created a new thread that is associated with the `std::thread` temporary object and assigned it to the `my_worker1` thread object.

```C++
my_worker1 = std::thread(my_other_work)
```
> When we call `std::thread` by only specifying the task, the `std::thread` constructor creates a temporary object and assigns the ownership of the newly created thread to that object. Now, this thread doesn't start it's execution immediately. We can start it by providing a thread object to it.
That's why I've used the **move assignment operator** to assign the returned thread object by the `std::thread` constructor, to the thread object `my_worker1`. As you can see we don't need to call `std::move` for such assignment operations.

Here, I've used the `get_id` function to print the underlying thread ID to show that the ownership is actually getting transferred.

> Now one thing to note, the thread ID may be different on your system, as it's determined by the OS and can vary from one run to the next. The value of the thread ID is not guaranteed to be unique(i.e. thread ID's can be recycled) or to have any particular meaning. It is simply a numerical identifier that is used to distinguish one thread from another within the same program.

For some reason, if you don't want to use `std::move` function, you can also use `std::thread::swap` to swap the ownership of the underlying threads.

``` 
1. thread_name.swap(other_thread_name);
or,
2. std::swap(thread_name, other_thread_name);
```
Now, I guess we should move to our next topic.

## Race Condition and Data Race:

Suppose you're facing such a scenario, where you've more than one concurrent thread, trying to access and modify the same shared data concurrently and depending on the order in which they access the data the final outcome differs. Now, this can cause reliability issues in the program, this is what we call a **Race Condition**.

**Data Races** occur when two or more threads access a common memory location simultaneously and at least one of them modifies the data resulting in undefined corrupt data at that memory location.

It is possible that Race Conditions can occur because of Data Race or vice versa.

Let's see this with an example.

_`Code Example:`_
```C++
#include <iostream>
#include <thread>

int shared_counter = 0;

void increment_counter() {
    for (int i = 0; i < 100000; i++) {
    //--critical region--//
        shared_counter++;
    }
}

int main() {
    std::thread my_worker1(increment_counter);
    std::thread my_worker2(increment_counter);

    my_worker1.join();
    my_worker2.join();

    std::cout << "Final counter value: " << shared_counter << std::endl;
    return 0;
}
```
Before I explain the code, it's important to know that when we call the `std::thread` object specifying the task, the C++ threading API uses the lower level API (***Win*****API** for Windows) provided by the OS to launch a thread of execution.

In the process of which, the OS allocates the associated kernel resources and stack space and then it adds the new thread to the scheduler. After that, the thread scheduler executes those threads depending on it's various algorithms(FCFS, Time Slicing, Priority Based).

> Different OS may use a different approach to schedule the threads. In case of Windows, it follows a priority-driven, preemptive scheduling system. Which means the threads scheduler can interrupt a running thread at any time and yield it's execution by putting it into a wait state to allow another thread with higher priority to execute.

Now that we have a bit of an idea about the underlying details let's get back to the code example.

_`Explanation:`_

So here, we have two concurrent threads `my_worker1` and `my_worker2` and both of them are working on the same `shared_counter` variable and doing increment operations for 100k times each. Cool!

It would be a fair guess if you think that the output will be 200k. But, after running the code quite a few times the outcome is nowhere near the expected result(At least for me). Now, why is it happening?

As I previously said, these two threads are running concurrently, so there is a chance that the progress made by one thread can be overridden by another thread because of the asynchronized interleaving instructions and the data race.

Here is the visualization of what might have happened.

<!--
![Race Condition and Data Race Visualization GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1w85ho65yt51js21jow.gif)
-->

![Race Condition and Data Race Visualization GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hqm6pnmj0al3wq2tnvds.gif)


Even though each thread has done the increment operation once but the final value stored in `shared_counter` is only 1, which is not the desired output.

**Disclaimer:** This visualization is only one of many scenarios that are possible, which means the order of these threads accessing the `shared_counter` variable can change. Now, run the for loop for 200k times and it ends up with complete chaos...

> As Race Condition depends on the relative ordering of the instructions, most of the time it won't cause any issues(i.e. Benign Race Condition) as long as the invariants are not broken. But a problematic race condition may result in a broken invariant (final value ≠ expected value).

So, it is evident that we need some kind of synchronization for our threads. This is where Mutual Exclusion or Mutex come into play!

## Thread Mutexes:

`std::mutex` class is a synchronization primitive that provides a locking mechanism which can be used to protect shared data from being simultaneously accessed by multiple threads.

_`Code Example:`_

```C++
#include <iostream>
#include <thread>
#include <mutex>

int shared_counter = 0;
std::mutex key;

void increment_counter() {
    for (int i = 0; i < 100000; i++) {
        key.lock();     // lock critical region
        shared_counter++;
        key.unlock();   // unlock critical region
    }
}

int main() {
    std::thread my_worker1(increment_counter);
    std::thread my_worker2(increment_counter);

    my_worker1.join();
    my_worker2.join();

    std::cout << "Final counter value: " << shared_counter << std::endl;
    return 0;
}
```

_`Explanation:`_

As you can see, we have created a `std::mutex` object called `Key`. By using the `lock()` and `unlock()` member functions of the `std::mutex` class, a thread (given that it reaches the mutex first) will be able to lock and unlock the `shared_counter` variable.

<!--
![Mutex Visualization GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w3ejsg6brhaqd6mgm5rv.gif)
-->

![Mutex Visualization GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ois556jecqdkeg8ry9a3.gif)

While the critical region is locked, other threads outside will be blocked until the critical region gets unlocked. Thereby the data race between these two threads will be avoided.

Blocking a thread in some cases may not be an optimal approach. To overcome that we can also use `std::mutex::try_lock()` rather than `std::mutex::lock()`.

```C++
void increment_counter() {
    for (int i = 0; i < 100000; i++) {
        if(key.try_lock()) { // try to lock critical region
            shared_counter++;
            key.unlock();   // unlock critical region
        }
    }
}
```
What this would do is instead of putting the thread in a block queue, it returns the boolean value `false`, allowing the thread to try again later.

Here, goes the visualization of what may happen if we introduce `try_lock()` in our code.

<!--
![try_lock() visualization GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yg2b1q8n7fjkwranbw0r.gif)
-->

![try_lock() visualization GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ek9x0mkvpjuyj94x2k50.gif)

> “With greater flexibility comes greater responsibility.”

Although, `try_lock()` is solving a key issue here but it may introduce unexpected code behaviours. Also if not implemented properly it may lead to a **livelock** situation where two or more threads are constantly trying and failing to acquire the lock leading to wasted CPU cycles.

That's why the C++ `std::thread` library provides a special kind of mutex class called `std::timed_mutex` which comes with the `std::timed_mutex::try_lock_for()` and `std::timed_mutex::try_lock_until()` member functions.

## Deadlock and Livelock:

A **deadlock** situation occurs when two or more blocked threads are waiting for each other to release a resource such as a mutex, which never gets released, resulting in the threads stopping their execution and waiting for an indefinite amount of time.

As bad as it sounds, deadlocks can easily hang our applications for an indefinite amount of time unless some external intervention occurs.

Let's try to recreate a deadlock situation,

_`Code Example:`_
<!--- Deadlock --->
```C++
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex cout_key;

void my_work() {
    cout_key.lock();
    std::cout << "Thread" << '[' << std::this_thread::get_id() << ']' << " acquired the lock" << '\n';
    //cout_key.unlock();
}

int main() {
    std::vector<std::thread> my_threads;
    for(int i=0; i<2; i++) {
        my_threads.emplace_back(my_work);
    }
    for(auto& i: my_threads) {
        i.join();
    }
    return 0;
}
```
One of the simplest kinds of deadlock can occur when a lock is acquired by a thread but never released.

![Deadlock Visualization GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hn97omuqw3x0e6vrbgf6.gif)

In this example, the thread that locks the mutex first will print the output without releasing the lock, causing the other thread to be blocked upon attempting to acquire the lock and unable to proceed further.

A **livelock** situation, on the other hand, is similar to the deadlock situation but in this case, instead of the threads getting blocked, they constantly run on an active checking loop consuming CPU resources without making any real progress.

_`Code Example:`_
<!--- Livelock --->
```C++
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex cout_key;

void my_work() {
    while(true) {
        if(cout_key.try_lock()) {
            std::cout << "Thread" << '[' << std::this_thread::get_id() << ']' << " acquired the lock\n";
            //cout_key.unlock();
            return;
        }
    }
}

int main() {
    std::vector<std::thread> my_threads;
    for(int i=0; i<2; i++) {
        my_threads.emplace_back(my_work);
    }
    for(auto& i: my_threads) {
        i.join();
    }
    return 0;
}
```

![Livelock Visualization GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k40pnvk03i2m5gqg0i3m.gif)

Similar to the previous scenario, the thread that acquires the `cout_key` mutex first will print the output without releasing it. As a result, the second thread will attempt to acquire the mutex and enter an infinite loop.

Whether it is a deadlock or a livelock, it is indeed one of the worst problems to face while developing any multithreaded applications. 

To avoid situations like this, we can follow some guidelines, such as keeping the critical section short, following a specific order of mutexes, and putting a time bound on the waiting part. 

We can also use some of the C++ features while working with locks, such as using RAII (i.e. scoped bound resource management) like `std::lock_guard`, or using `std::scoped_lock` to acquire more than one lock.

> "Prevention is better than a cure"

In conclusion, threads can be a bit of a hassle to debug, and that's why it is important to follow the best practices from the very beginning so that at the end it doesn't become unmanageable and time-consuming to identify and fix bugs.

<!--- 
To-do:
     Conditional Variable
     Synchronization of Threads and Processes
Done:
     Move Ownership of threads
     Thread Id //use this method while explaining other topic
     Race Condition and Mutexes (Do the ppt animated gifs for try_lock() and timed mutex)
     Deadlock and Livelocks

and provides a higher level interface for the user to control the threads using the thread object.
 --->