Multithreading enables us to do parallel computing efficiently by creating multiple threads of a single process. It is used for maximum utilization of the central processing unit by concurrently executing multiple parts of a program. These parts of the program are threads.
A process can be taken as an instance of a program that is being executed by one or more threads. When a program starts, it is loaded with an address space in the main memory from the hard disk. After being loaded on the memory it is being scheduled to an available CPU. Now when a process is scheduled on CPU, process instructions execute sequentially. There is a program counter to keep track of instructions to be executed. The program counter represents the execution context. Execution context is the state of a running process which indicates data like which instruction CPU is executing. We refer to these execution contexts as threads.
There can be multiple threads in a process. These threads can execute different sections of the program simultaneously and can be scheduled on single processor or multiprocessor and multicore systems. A process has its own address space while a thread shares the address space of its parent process along with all the threads its parent process has created. Threads are considered lightweight data structures compared to the process because they will always consume almost less memory than a new process would. Let’s visualize it a bit.
Suppose we are getting some value to variable A, and there is a possibility that each thread in process P can set the value. The programmer has no control over which thread accesses a variable A and when does it access that variable. The operating system scheduler schedules processes and threads according to some scheduling algorithms ( Scheduling algorithms - First Come First Serve, Shortest Job First, Shortest Remaining Time First, Round Robin Scheduling, Priority Based Scheduling ).
To construct multithreaded programs. We need to construct synchronization constructs. Some of them are mutexes and semaphores. These mechanisms enforce synchronization between threads. So if a thread T2 in process P is initializing A no other thread is allowed to initialize A.
Multiprocessing is the concept of using two or more central processing units to run a program. Multiple processes are executed simultaneously. By multiprocessing, we can avoid the problem of synchronization between threads in a multithreaded process because each process has its own address space that other processes are not allowed to write as the operating system actively prevents the process from writing to another process address space. However, address spaces can be very large and we may only need to run just a portion of the program. If we were to create many processes for parallel computing we would quickly use memory on any machine and so threads are useful. It is also not economical to have multiple processing units. The advantage is bigger than the disadvantage so we use multithreading for efficient parallel computing instead of multiprocessing.
We have the two main functions of creating a thread and waiting for the thread to finish execution.
#include<thread>
To start a thread we need to create a new thread object and pass it a callable as an argument to its constructor. Callable is an executable code that we want to execute when the thread is running.
We can define a callable in three ways:
We need to define a class and in that class we overload an operator. The overloaded function is having the code which is to be executed.
#include <iostream>
#include <thread>
using namespace std;
// Define the class of function object
class func_obj
{
public:
void operator()(parameters) // Overload () operator
{
// Code to be executed
}
};
int main()
{
// Create thread object
thread thread_object(func_obj, parameters);
// wait for thread thread_object to finish
thread_object.join();
return 0;
}
We need to define a function, then we can create a thread object with this function as callable. Parameters are passed after the function name.
#include <iostream>
#include <thread>
using namespace std;
// Define a function
void func_point(parameter_1, parameter_2
{
//Code to be executed
}
int main()
{
// Create thread object
thread thread_object(func_point, parameter_1,
parameter_2);
// wait for thread thread_object to finish
thread_object.join();
return 0;
}
Note: Parameters can be a variable, list, or vector etc.
We will define a lambda expression, and we will then pass it to the thread object constructor as the first argument followed by its parameters as further arguments.
#include <iostream>
#include <thread>
using namespace std;
int main()
{
// Defining a lambda expression
auto func_lambda =[](parameters)
{
// Code to be executed
};
// Create thread object
thread thread_object(func_lambda, parameters);
// wait for thread thread_object to finish
thread_object.join();
return 0;
}
Note: We can also pass lambda function directly to constructors.
#include <iostream>
#include <thread>
using namespace std;
int main()
{
//Create thread object
thread thread_object([], (parameters)
{
// Code to be executed
};, parameters
};
// wait for thread thread_object to finish
thread_object.join();
return 0;
}
This function is used to wait for a thread to finish before we can perform any other action.
#include <iostream>
#include <thread>
using namespace std;
int main()
{
//Create thread object
thread thread_object(callable)
// wait for thread thread_object to finish
thread_object.join();
// thread_objec is finished, we can do other things
now.
return 0;
}
#include <iostream>
#include <thread>
using namespace std;
// function pointer
void func_point(int i)
{
for (int j = 0; j < i; j++)
{
cout << "Thread with function pointer as callable." << endl;
}
}
// function object
class func_obj
{
public:
void operator()(int x)
{
for (int j = 0; j < x; j++)
{
cout << "Thread with function object as callable" << endl;
}
}
};
int main()
{
//This thread is created by function pointer as callable
thread T1(func_point, 5);
// This thread is created by function object as callable
thread T2(func_obj, 4);
//This thread is created by lambda expression as callable
thread T3([], (int y)
{
for (int j = 0; j < y; j++)
{
cout << "Thread with lambda expression as callable" << endl;
}
};, 4
};
// wait for thread T1 to finish
T1.join();
//wait for thread T2 to finish
T2.join();
//wait for thread T3 to finish
T3.join();
return 0;
}
Output:
Thread with function pointer as callable
Thread with function pointer as callable
Thread with lambda expression as callable
Thread with lambda expression as callable
Thread with function object as callable
Thread with function pointer as callable
Thread with function object as callable
Thread with lambda expression as callable
Thread with function object as callable
Thread with function pointer as callable
Thread with function object as callable
Thread with function pointer as callable
Thread with lambda expression as callable
To compile program with thread support we have to use:
g++ -std=c++11 -pthread
Multithreading is a very important feature which helps in efficiently computing multiple tasks parallely. It provides better resource utilization, simpler program design and more responsive programs. It is easy to implement and can help a lot in providing security and efficiency to our real world problems.
Help us improve this content by editing this page on GitHub