Open In App

Thread Synchronization in C++

Last Updated : 01 Jan, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

In C++ multithreading, synchronization between multiple threads is necessary for the smooth, predictable, and reliable execution of the program. It allows the multiple threads to work together in conjunction by having a proper way of communication between them. If we do not synchronize the threads working on a shared resource properly, it may cause some of the concurrency problems.

Importance of Thread Synchronization

Consider that there is a program that is being used for managing the banking data. Now, assume that two threads are being used for credit and debit in a bank account. Now, assume the following sequence of transactions of a user:

  1. Initial Balance = 300
  2. Credited_process: 400, Current Balance = 700
  3. Debited_process: 500, Current Balance = 200

Now, assume that there is no synchronization between the processes running on the different threads. So, it is possible that the sequence of the operation can be:

  1. Initial Balance: 300
  2. Debited_process: 500, Current Balance = 300
  3. Credit_process: 400, Current Balance = 700

Due to this change in the sequence, the user wont be able to withdraw his/her money from the bank account even though he had credited enough money in the account. Here, both credit_process and debit_process threads can be synchronized so that when there is a simultaneous credit and debit requests, it should first execute the credit request.

There can be many such concurrency issues that arises due to non-synchronized operations. Some of them are:

  1. Race Condition
  2. Deadlocks
  3. Starvation

Thread Synchronization in C++

In C++, thread synchronization is possible using the following methods:

1. Mutex in C++

Mutex is a synchronization primitive that locks the access to the shared resource if some thread is already accessing it.

Example

Let us take an example of a code snippet given below –

C++




// C++ program to illustrate the execution of multithreading
// program without any synchronization
#include <iostream>
#include <thread>
using namespace std;
  
// shared data
double val = 0;
int cnt = 0;
  
void add(double num)
{
    val += num;
    cnt++;
    cout << "Thread " << cnt << ": " << val << endl;
}
  
// driver code
int main()
{
    thread t1(add, 300);
    thread t2(add, 600);
    t1.join();
    t2.join();
    cout << "After addition : " << val << endl;
    return 0;
}


Output

Thread Thread 22: : 900900

After addition : 900

In the above code snippet, we created two threads and we have an add function which in turn adds the values. Both the threads, t1 and t2 go to the add function simultaneously. Now, it becomes difficult to decide which thread will execute first and modify the value of num as both of them move to the add function simultaneously. Such a condition is called race condition. Thus, we should apply thread synchronization to avoid this race condition.

Here, we will apply mutex to achieve synchronization.

C++




// C++ program to illustrate the use of mutex locks to
// synchronize the threads
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
  
// shared lock
double val = 0;
  
// mutex lock
mutex m;
  
int cnt = 0;
  
void add(double num)
{
    m.lock();
    val += num;
    cnt++;
    cout << "Thread " << cnt << ": " << val << endl;
    m.unlock();
}
  
// driver code
int main()
{
    thread t1(add, 300);
    thread t2(add, 600);
    t1.join();
    t2.join();
    cout << "After addition : " << val << endl;
    return 0;
}


Output

Thread 1: 300
Thread 2: 900
After addition : 900

or

Thread 1: 600
Thread 2: 900
After addition : 900

Note: In the above code after applying mutex we can get any of the two outputs as shown above. This is because, after applying mutex we have prevented both the threads from entering inside the add() function together, but, either thread t1 or thread t2 enters the add() function first and therefore the output varies with respect to that.

Now, both the threads will not be able to access the critical section at the same time as we have applied lock . Thus, here using mutex we were able to achieve thread synchronization.

2. Condition Variable in C++

The condition variable is another such synchronization primitive but it is mainly used to notify the threads about the state of the shared data. It is used with the mutex locks to create processes that automatically wait and notify the state of the resource to each other.

Example

C++




// C++ program to illustrate the use of condition variable
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
  
using namespace std;
  
// condition variable and mutex lock
condition_variable cv;
mutex m;
  
// shared resource
int val = 0;
  
void add(int num)
{
    lock_guard<mutex> lock(m);
    val += num;
    cout << "After addition: " << val << endl;
    cv.notify_one();
}
  
void sub(int num)
{
    unique_lock<mutex> ulock(m);
    cv.wait(ulock,
            [] { return (val != 0) ? true : false; });
    if (val >= num) {
        val -= num;
        cout << "After subtraction: " << val << endl;
    }
    else {
        cout << "Cannot Subtract now!" << endl;
    }
    cout << "Total number Now: " << val << endl;
}
  
// driver code
int main()
{
    thread t2(sub, 600);
    thread t1(add, 900);
    t1.join();
    t2.join();
    return 0;
}


Output

After addition: 900
After subtraction: 300
Total number Now: 300

Explaination

Here, in this program we first create two threads and then we try to perform addition first followed by subtraction. But as we can see in the above program, we passed the thread t2 first and then t1. Assuming thread t2 goes to the sub() function first, it first locks the mutex and then checks the condition whether the val is 0 or not. As the val is 0 initially, the predicate returns false and as soon as it returns false, it releases the mutex and waits for the condition to be true i.e., val!=0. Now as the mutex is released, addition is performed in the add() function and after that notify_one() gets executed which notifies the waiting thread which in turn tries to get the lock and again checks the condition. This way the process continues.

One of the best use case of condition variable is Producer-Consumer Problem.

3. Promise and Future

The std::future and std::promise are used to return the data from a task executed on the other thread. The std::promise is used to sent the data and the std::future is used to receive the data on the main process. The std::future get() method can be used to retrieve the data returned by the process and is able to hold the current process till the value is returned.

This method is generally preferred of over the condition variable when we only want the task to be executed once.

Example

C++




// C++ program to illustrate the use of std::future and
// std::promise in thread synchronization.
#include <future>
#include <iostream>
#include <thread>
using namespace std;
  
// callable
void EvenNosFind(promise<int>&& EvenPromise, int begin,
                 int end)
{
    int evenNo = 0;
    for (int i = begin; i <= end; i++) {
        if (i % 2 == 0) {
            evenNo += 1;
        }
    }
    EvenPromise.set_value(evenNo);
}
  
// driver code
int main()
{
    int begin = 0, end = 1000;
    promise<int> evenNo;
    future<int> evenFuture = evenNo.get_future();
    cout << "My thread is created !!!" << endl;
    thread t1(EvenNosFind, move(evenNo), begin, end);
    cout << "Waiting..........." << endl;
  
    // detting the data
    cout << "The no. of even numbers are : "
         << evenFuture.get() << endl;
    t1.join();
    return 0;
}


Output

My thread is created !!!
Waiting...........
The no. of even numbers are : 501

Here, in the above program, we try to find the number of even numbers in the given range. We first create a promise object and then we create a future object from that promise object. We send the promise object to the thread and then once we are ready with the value (after the function has been executed) ,we set the promise object. Then we create a future object from that promise. Finally we get the output from the future object to get our answer.

Conclusion

Thread Synchronization is necessary in the cases when we have multiple threads working on the shared data(critical section) so that the behaviour is defined. We have seen few common method of thread synchronization in C++. Although, it can be done manually using naive and bruteforce methods like (continuously updated some flag when done when done working on the shared data), the above discussed method provides more refined and tested ways of thread synchronization in C++.



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads