Tutorial Study Image

C++ Multithreading


June 15, 2023, Learn eTutorial
264

Multithreading: What is it?

Multithreading is the capacity of a platform (Operating System, Virtual Machine, etc.) or application to generate a process that consists of many threads of execution (threads). The smallest group of instructions that can be separately controlled by a scheduler is referred to as a thread of execution. These threads can execute in parallel, which improves program efficiency.

Multithreading refers to the execution of several threads on multiple cores or processors at the same time in multicore and multiprocessor systems.

The time is divided across the threads in single-core computers via multithreading. A predetermined number of instructions are sent to the CPU by the operating system from each thread. It is not possible to run many threads at once. The operating system just simulates their concurrent execution. This operating system capability is known as multithreading.

In C++11, built-in multithreading capability was made available. Multithreaded C++ program development is made achievable via the header file thread.h.

Features of Multithreading in C++

  • Multitasking is the ability of our computer to execute two or more programs at the same time and multithreading is a specific type of multitasking. Process-based multitasking and thread-based multitasking are the two main types of multitasking.
  • Concurrent program execution is handled by process-based multitasking. The concurrent execution of various parts of the same program is what thread-based multitasking is all about.
  • A multithreaded program has two or more concurrently running components. In such a program, each component is referred to as a thread, and each thread specifies a unique path of execution.
  • Multithreading is used when the concurrent execution of some tasks leads to more efficient use of system resources.
  • Multithreaded applications are not supported natively in C++ prior to version 11. The operating system is completely responsible for providing this capability.

Why multithreading is used?

A multithreaded application is one that runs multiple threads simultaneously within the application itself.

For example, if we want to design a server that can handle as many concurrent connections as the server can handle, we can achieve this fairly quickly if we create a new thread for each connection. In some instances, a multithreaded server starts a new thread for every incoming connection instead of making a separate socket to handle them, and each new thread also makes a new socket.

The need for multithreading in GUI applications is another common example. GUI applications execute one operation at a time in a single thread (the Main Thread). This thread is either processing an event or waiting for one. As a result, the user interface will freeze while a time-consuming activity is being initiated by the user. Multithreading needs to be used in this situation.

Through the creation of objects belonging to a Thread subclass, the main thread can initiate new threads. When an application is multithreaded, the GUI operates in its own thread while additional processing occurs in other threads. This ensures that the application's GUIs remain responsive even when processing is busy.

How do we create a thread?

To start, make sure your program has a thread header:

Syntax


#include <thread>
 

To make a thread, you must first construct an object of the thread class.


// This thread is not a representation of any execution thread.
thread t_empty;
 

As you can see, we don't give any data to the thread when the default constructor of the thread class is used. This indicates that in this thread, nothing is executed. A thread needs to be started. There are numerous ways to accomplish it.

A function pointer can be passed to a thread's function constructor when you create it. As soon as a thread is created, this function begins working in that thread. Consider the following example:


#include <iostream>
#include <thread> 
using namespace std;
void threadFunc()
{
  cout << "Welcome all to Multithreading" << endl;
}
int main()
{
  //pass a function to the thread
  thread funcTest1(threadFunc);
}

 

You will get a runtime problem even though it compiles without any issues

You can see that the main thread establishes funcTest1 as a new thread with the parameter threadFunc. The main thread doesn't wait for funcTest1 to finish. It keeps working. Even when the main thread has finished running, funcTest1 is still active. It leads to errors. Before the primary thread is terminated, all other threads must also be ended.

Join threads

Thread joining is accomplished by using the thread class's join() member function:

Syntax


void join();
 

This method only returns once all threads have been terminated. The main thread will wait until the child thread has completed its execution, in other words.

Terminating Threads

To end a POSIX thread, use the method listed below.


#include <pthread.h>
pthread_exit (status) 

Here, a thread is intentionally terminated using pthread exit. The pthread exit() routine is often invoked when a thread has finished its task and is no longer needed.

The other threads will continue to run even if main() exits with pthread exit() before all of the threads it has created have finished. If not, they will be ended automatically after main() is finished.

Example

Using the pthread create() function, this straightforward sample code creates 5 threads. Each thread ends by calling pthread_exit() after printing the message "Hello World!"

Example program using the pthread create() function


#include <iostream>
#include <cstdlib>
#include <pthread.h>

using namespace std;

#define NUM_THREADS 5

void *PrintHello(void *threadid) {
   long tid;
   tid = (long)threadid;
   cout << "Hello World! Thread ID, " << tid << endl;
   pthread_exit(NULL);
}

int main () {
   pthread_t threads[NUM_THREADS];
   int rc;
   int i;
   
   for( i = 0; i < NUM_THREADS; i++ ) {
      cout << "main() : creating thread, " << i << endl;
      rc = pthread_create(&threads[i], NULL, PrintHello, (void *)i);
      
      if (rc) {
         cout << "Error:unable to create thread," << rc << endl;
         exit(-1);
      }
   }
   pthread_exit(NULL);
}

 

Output:


main() : creating thread, 0
main() : creating thread, 1
main() : creating thread, 2
main() : creating thread, 3
main() : creating thread, 4
Hello World! Thread ID, 0
Hello World! Thread ID, 1
Hello World! Thread ID, 2
Hello World! Thread ID, 3
Hello World! Thread ID, 4

The above program should be built with the -lpthread library. $gcc test.cpp -lpthread

Execute your program now, and it will produce the results listed above.

Passing Arguments to Threads

With the use of a structure, this example demonstrates how to pass numerous arguments. A thread callback accepts any data type because it points to a void, as seen in the sample below.

Example program which passes arguments to Threads


#include <iostream>
#include <cstdlib>
#include <pthread.h>

using namespace std;

#define NUM_THREADS 5

struct thread_data {
   int  thread_id;
   char *message;
};

void *PrintHello(void *threadarg) {
   struct thread_data *my_data;
   my_data = (struct thread_data *) threadarg;

   cout << "Thread ID : " << my_data->thread_id ;
   cout << " Message : " << my_data->message << endl;

   pthread_exit(NULL);
}

int main () {
   pthread_t threads[NUM_THREADS];
   struct thread_data td[NUM_THREADS];
   int rc;
   int i;

   for( i = 0; i < NUM_THREADS; i++ ) {
      cout <<"main() : creating thread, " << i << endl;
      td[i].thread_id = i;
      td[i].message = "This is message";
      rc = pthread_create(&threads[i], NULL, PrintHello, (void *)&td[i]);
      
      if (rc) {
         cout << "Error:unable to create thread," << rc << endl;
         exit(-1);
      }
   }
   pthread_exit(NULL);
}

 

Output:


main() : creating thread, 0
main() : creating thread, 1
main() : creating thread, 2
main() : creating thread, 3
main() : creating thread, 4
Thread ID : 3 Message : This is message
Thread ID : 2 Message : This is message
Thread ID : 0 Message : This is message
Thread ID : 1 Message : This is message
Thread ID : 4 Message : This is message

Joining and Detaching Threads

The following two procedures can be used to attach or detach threads:


pthread_join (threadid, status) 
pthread_detach (threadid) 
 

The pthread join() method pauses the calling thread until the given 'threadid' thread terminates/ends. It is determined whether a thread is joinable or detached when it is generated by one of its attributes. A thread can only be joined if it was created as a joinable thread. A disconnected thread cannot ever be joined after being formed.

Using the Pthread join method, the following example shows how to wait for thread completions.

 


#include <iostream>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>

using namespace std;

#define NUM_THREADS 5

void *wait(void *t) {
   int i;
   long tid;

   tid = (long)t;

   sleep(1);
   cout << "Sleeping in thread " << endl;
   cout << "Thread with id : " << tid << "  ...exiting " << endl;
   pthread_exit(NULL);
}

int main () {
   int rc;
   int i;
   pthread_t threads[NUM_THREADS];
   pthread_attr_t attr;
   void *status;

   // Initialize and set thread joinable
   pthread_attr_init(&attr);
   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

   for( i = 0; i < NUM_THREADS; i++ ) {
      cout << "main() : creating thread, " << i << endl;
      rc = pthread_create(&threads[i], &attr, wait, (void *)i );
      if (rc) {
         cout << "Error:unable to create thread," << rc << endl;
         exit(-1);
      }
   }

   // free attribute and wait for the other threads
   pthread_attr_destroy(&attr);
   for( i = 0; i < NUM_THREADS; i++ ) {
      rc = pthread_join(threads[i], &status);
      if (rc) {
         cout << "Error:unable to join," << rc << endl;
         exit(-1);
      }
      cout << "Main: completed thread id :" << i ;
      cout << "  exiting with status :" << status << endl;
   }

   cout << "Main: program exiting." << endl;
   pthread_exit(NULL);
}

Output:



main() : creating thread, 0
main() : creating thread, 1
main() : creating thread, 2
main() : creating thread, 3
main() : creating thread, 4
Sleeping in thread
Thread with id : 0 .... exiting
Sleeping in thread
Thread with id : 1 .... exiting
Sleeping in thread
Thread with id : 2 .... exiting
Sleeping in thread
Thread with id : 3 .... exiting
Sleeping in thread
Thread with id : 4 .... exiting
Main: completed thread id :0  exiting with status :0
Main: completed thread id :1  exiting with status :0
Main: completed thread id :2  exiting with status :0
Main: completed thread id :3  exiting with status :0
Main: completed thread id :4  exiting with status :0
Main: program exiting.

Multithreading vs. Multiprocessing

Multiprocessing

A process is a resource allocation and protection unit. Some resources, including as virtual memory, I/O handlers, and signal handlers, are managed by processes. Using an MMU, the process is shielded from other processes. Cons: Inter-process communication (IPC) might be difficult and ineffective.

Multithreading

The computational unit that operates within a process is referred to as a thread. A thread is responsible for managing a variety of resources, including the stack, registers, signal masks, priorities, and thread-specific data. pros: IPC between processes is less effective than IPC between threads. Cons: Threads may conflict with one another.