Threads in C++


Reading time: 35 minutes | Coding time: 10 minutes

Threads are generally referred as light weight processes. Each thread executes different parts of a program. Each thread shares memory,file descriptors and other system resources. In simple words, every process start with main thread and in that main thread we creates multiple threads

Untitled-1

In main function we can create a thread which will have a entry point function the threadFunction() and we can wait for the thread to get completed and when the entry point function gets completed the control goes back to the main thread.

Syntax

#include <thread>

int main(){
    thread t1;
}

We just need to include the thread library to create a thread. A simple example would be writing thread t1 to create a thread named t1, additional options for the created thread can be passed as arguments to the constrctor already defined in the thread library.
Example -

thread t1 (threadFunction, valueToThreadFunction)

Multithreading

Multithreading means two or more threads running concurrently where each thread is handling a different task. In simple words multithreading is multitasking that allows computer to run two or more programs concurrently.In general, there are two types of multitasking: process-based and thread-based.

  • Process-based multitasking handles the concurrent execution of programs.
  • Thread-based multitasking deals with the concurrent execution of pieces of the same program.

C++ does not contain any built-in support for multithreaded applications.Instead,it relies entirely upon the operating system to provide this feature.

Creation of thread

When creating a thread,something need to be passed to be executed on it.A few things that can be passed to a thread:

  • Free functions
  • member functions
  • Functor objects
  • Lambda expressions

Free function

Free function example - executes a function on a separate thread

#include <string>
#include <iostream>
#include <thread>
using namespace std;

void threadFunction(int value){
    cout<<"value->"<<value<<endl;
}
int main(){
	//creating thread and passing argument to Thread
	thread t1(threadFunction,1000);

	//wait for thread t1 to complete
	t1.join();

	return 0;
}

Member function

Member function example - executes a member function on a separate thread

#include <iostream>
#include <thread>
using namespace std;

class fun
{
    public:
        void threadFunction(int value)
        {
            cout <<"value->"<<value <<endl;
        }
};

int main()
{
    fun threadFunction;
    // Create and execute the thread
    thread t1(&fun::threadFunction, &threadFunction, 10); 
    
    // The member function will be executed in a separate thread
    
    // Wait for the thread to finish, this is a blocking operation
    t1.join();
return 0;
}

Functor object

Functor object example

#include <iostream>
#include <thread>
using namespace std;
class fun
{
    public:
        void operator()(int value)
        {
               cout<<"value->"<<value <<endl ;
        }
};
int main()
{
    fun threadFunction;
    // Create and execute the thread
    thread t1(threadFunction, 10);
    
    // The functor object will be executed in a separate thread
    
    // Wait for the thread to finish, this is a blocking operation
    t1.join();
    return 0;
}

Lambda expression

Lambda expression example

#include <iostream>
#include <thread>
using namespace std;
int main()
{
    auto lambda = [](int value) { cout<<"value->"<<value <<endl ; };
    // Create and execute the thread
    thread t1(lambda, 10);
    
    // The lambda expression will be executed in a separate thread
    
    // Wait for thread to finish, this is a blocking operation
    t1.join();
    return  0;
}

Race condition

A race condition occurs when two or more threads can access shared data and they try to change it at the same time.In simple words, When two or more threads perform a set of operations in parallel, that access the same memory location. Also, one or more thread out of them modifies the data in that memory location, then this can lead to an unexpected results some times.Let's try to understand it with an example

class Wallet
{
    int Money;
    public:
    Wallet() :Money(0){}
    int getMoney() { return Money; }
    void addMoney(int money){
        for(int i = 0; i < money; ++i){
            Money++;
        }
    }
};

Now Let’s create 5 threads and all these threads will share a same object of class Wallet and add 1000 to internal money using it’s addMoney() member function in parallel.So if initially money in wallet is 0. Then after completion of all thread’s execution money in Wallet should be 5000.But as all threads are modifying the shared data at same time, it might be possible that in some scenarios money in wallet at end will be much lesser than 5000.

int testMultithreadedWallet(){
    Wallet walletObject;
    std::vector<std::thread> threads;
    for(int i = 0; i < 5; ++i){
        threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
    }
    for(int i = 0; i < threads.size() ; i++){
        threads.at(i).join();
    }
    return walletObject.getMoney();
}

int main(){
   int val = 0;
   for(int k = 0; k < 1000; k++){
     if((val = testMultithreadedWallet()) != 5000){
       std::cout << "Error at count = "<<k<<" Money in Wallet = "<<val << std::endl;
        }
    }
    return 0;
}

As addMoney() member function of same Wallet class object is executed 5 times hence it’s internal money is expected to be 5000. But as addMoney() member function is executed in parallel hence in some scenarios Money will be much lesser than 5000 i.e. output is

Error at count = 971  Money in Wallet = 4568                                         Error at count = 971  Money in Wallet = 4568                                         Error at count = 972  Money in Wallet = 4260                                    Error at count = 972  Money in Wallet = 4260                                    Error at count = 973  Money in Wallet = 4976                                          Error at count = 973  Money in Wallet = 4976

This is a race condition, as here two or more threads were trying to modify the same memory location at same time and lead to unexpected result.

How to fix Race Conditions?

To fix this problem we need to use Lock mechanism i.e. each thread need to acquire a lock before modifying or reading the shared data and after modifying the data each thread should unlock the Lock.

Mutex

The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads.C++ offers a selection of mutex classes:

  • std::mutex - offers simple locking functionality.
  • std::timed_mutex - offers try_to_lock functionality
  • std::recursive_mutex - allows recursive locking by the same thread.
  • std::shared_mutex, std::shared_timed_mutex - offers shared and unique lock
    functionalities
#include <iostream>
#include <thread>
#include <mutex>
#include <shared_mutex>
#include <chrono>
 
using namespace std;
 
void ThreadFn (mutex &mtx){
    lock_guard<mutex> lock(mtx);
    cout<<"Mutex is locked"<<endl;
    this_thread::sleep_for(chrono::seconds(5));
}
 
int main(){
    mutex mtx;
    thread th(ThreadFn, ref(mtx));
    this_thread::sleep_for(chrono::seconds(1));
    unique_lock<mutex> lock(mtx);
    cout<<"Main thread active"<<endl;
 
    th.join();
    return 0;
 
}

Firstly thread th() gets executed and then it sleeps for 1sec avoiding any race condition.When thread th() sleeps the mutex lock get successful in ThreadFn which print "Mutex is locked" and the thread sleeps for 5sec.In the mean time unique_lock try to lock the mutex but it cannot locked the mutex because lock_guard locked the mutex so it will wait for 5sec and then unlock the mutex.As soon as mutex is unlocked it get locked by Unique_lock and print "main thread is active".Thus, the mutual exclusion works in this fashion.