C++ Concurrency in action
C++ Concurrency in action
The ability of a program to complete multiple tasks simultaneously is referred to as concurrency.
The thread class in the standard library in C++ supports concurrency. A thread is the fundamental execution unit of a program and can run alongside other threads. In this tutorial, we'll delve into great detail about concurrency in C++.
A strong and adaptable programming language is C++. Concurrency is one of the many programming paradigms that it supports. Concurrency is the capacity of a software to run many threads of execution simultaneously.
Performance and responsiveness are enhanced, especially in programs that use CPU- or I/O-bound tasks.
Threads, mutexes, condition variables, and futures are just a few of the concurrency programming capabilities and libraries that are integrated into C++.
Some of them are:
- Threads
- Mutexes
- Condition Variables
- Futures
- Multithreading
- Parallelism:
- Synchronization:
- Concurrency Patterns:
1. Threads
It is a set of commands that can be carried out separately from the primary program. The std::thread class from the standard library can be used to generate the thread in C++. Let's use the example below to understand this.
Program:
#include<iostream>
#include<thread>
// Function to be executed in the thread
void threadFunction() {
std::cout<<"Thread running"<<std::endl;
}
int main(){
// Create a thread object and pass the function to be executed
std::thread myThread(threadFunction);
myThread.join();
std::cout<<"Thread finished"<<std::endl;
return 0;
}
Output:
![C++ Concurrency in action](https://static.tutorialandexample.com/cpp/cpp-concurrency-in-action1.png)
Explanation:
The threadFunction function is supplied as an argument to the thread in the example above, which creates a distinct thread using the std::thread class. A straightforward function called threadFunction outputs the message "Thread running" to the console.
Before continuing with the rest of the program, the join() method is used to wait for the thread to complete its execution. The main thread then writes "Thread finished" to signify that it has finished.
2. Mutexes
A mutex is a synchronization object that is useful for preventing concurrent access to shared resources by different threads. The C++ standard library's std::mutex class can be used to create mutexes. Let's use the example below to understand this.
Program:
#include<iostream>
#include<thread>
#include<mutex>
std::mutex mtx; // Mutex for protecting shared resource
void sharedResourceAccess()
{
std::lock_guard<std::mutex> lock(mtx); // Lock the mutex
// Access the shared resource
std::cout<<"Thread"<<std::this_thread::get_id()<<"is accessing the shared resource."<< std::endl;
// Simulate some work
std::this_thread::sleep_for(std::chrono::seconds(2));
// Release the mutex automatically when lock_guard goes out of scope
}
int main()
{
const int numThreads = 5;
std::thread threads[numThreads];
// Create multiple threads to access the shared resource
for(int i=0;i<numThreads;++i){
threads[i]=std::thread(sharedResourceAccess);
}
for(int i=0;i<numThreads;++i){
threads[i].join();
}
return 0;
}
Output:
![C++ Concurrency in action](https://static.tutorialandexample.com/cpp/cpp-concurrency-in-action2.png)
Explanation:
The std::mutex class from the C++ Standard Library is used in this program to synchronize access to a shared resource. The mutex is automatically locked and unlocked using the std::lock_guard class.
Each thread executes the sharedResourceAccess function, which acquires the lock, performs some work (simulated by a sleep), and releases the lock.
The output will show that the threads access the shared resource in a serialized manner due to the mutex. The exact order of thread execution may vary.
3. Condition Variables
A thread is blocked by a condition variable, a synchronization object, until a specific condition is met.
The standard library's std::condition_variable class in C++ can be used to construct condition variables. Let's use the example below to understand this.
Program:
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool isReady = false;
void workerThread()
{
std::cout<<"Worker thread is running..."<<std::endl;
// Simulate some work
std::this_thread::sleep_for(std::chrono::seconds(2));
// Set the flag to indicate work is done
{
std::lock_guard<std::mutex> lock(mtx);
isReady = true;
}
// Notify the waiting thread
cv.notify_one();
}
int main()
{
std::cout << "Main thread started..." << std::endl;
// Create a worker thread
std::thread worker(workerThread);
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return isReady; });
}
std::cout << "Work is completed!" << std::endl;
// Join the worker thread
worker.join();
std::cout << "Main thread exited." << std::endl;
return 0;
}
Output:
![C++ Concurrency in action](https://static.tutorialandexample.com/cpp/cpp-concurrency-in-action3.png)
Explanation:
In this example, we have a worker thread that simulates doing some work and sets a flag (isReady) to indicate that the work is completed. The main thread waits for the work to be completed using a condition variable (cv) and a mutex (mtx).
The condition variable is not informed while the main thread waits by using cv.wait(), which releases the mutex and puts the main thread to sleep.
The main thread wakes up, takes the lock on the mutex, and continues execution after the worker thread notifies it that its task is finished by executing cv.notify_one().
4. Futures
It is a synchronization object that can be used to get data from an asynchronously running thread or function. The C++ standard library's std::future class can be used to generate a future. Let's use the example below to understand this.
Program:
#include<iostream>
#include<future>
// Function that performs a time-consuming task
int performTask()
{
// Simulating a time-consuming task
std::this_thread::sleep_for(std::chrono::seconds(5));
return 42;
}
int main()
{
// Creating a future object to hold the result of the task
std::future<int> futureResult = std::async(std::launch::async, performTask);
std::cout << "Waiting for the result..." << std::endl;
// Wait for the result and retrieve it
int result = futureResult.get();
std::cout << "The result is: " << result << std::endl;
return 0;
}
Output:
![C++ Concurrency in action](https://static.tutorialandexample.com/cpp/cpp-concurrency-in-action4.png)
Explanation:
This software contains a function called performTask() that, by putting the current thread to sleep for 5 seconds and then returning the result 42, simulates a time-consuming work.
We use std::async to generate a futureResult std::futureint> object in the main() method. The performTask() method is executed by a new thread that is created by the std::async function. It gives back a std::future object that eventually contains the task's outcome.
We then wait for the job to finish and retrieve its output using the get() member function of the std::future object.
5. Multithreading
It is the capacity to run several threads of code simultaneously in a single application. The independent execution routes known as threads allow for parallel operation. Applications that demand parallelism, like video encoding, scientific simulations, or graphics rendering, can benefit from it.
The thread class from the Standard Template Library (STL) in C++ is useful for multithreading.
Program:
#include<iostream>
#include<thread>
// Function to be executed by the thread
void threadFunction()
{
for(int i=0;i<5;++i)
{
std::cout << "Thread executing..." << std::endl;
}
}
int main()
{
// Create a thread object and pass the function to be executed
std::thread t(threadFunction);
for(int i=0;i<3;++i)
{
std::cout << "Main thread executing..." << std::endl;
}
// Wait for the thread to finish
t.join();
return 0;
}
Output:
![C++ Concurrency in action](https://static.tutorialandexample.com/cpp/cpp-concurrency-in-action5.png)
Explanation:
The threadFunction is given as the function to be executed in the new thread when a new thread is created in this application using the std::thread class. Simply put, the threadFunction prints the same message five times.
The main thread also prints a different message three times in the meantime. The software then waits for the thread to finish before exiting by calling the join() function after that.
The output from the main thread and the newly formed thread should be interspersed when you execute this program, illustrating concurrent processing.
6. Parallelism:
The capacity to carry out several tasks concurrently via multiple threads or processes is known as parallelism.
Applications that need great performance and efficiency, such data processing, scientific simulations, or machine learning, benefit from parallelism. The Parallel STL package or OpenMP are two methods for implementing parallelism in C++.
Program:
#include<iostream>
#include<vector>
#include<omp.h>
int main() {
// Number of elements in the array
const int size = 10;
// Input array
std::vector<int> array={1,2,3,4,5,6,7,8,9,10};
// Output array to store the squares
std::vector<int> squares(size);
// Parallelize the loop using OpenMP
#pragma omp parallel for
for(int i=0;i<size;++i){
squares[i] = array[i] * array[i];
}
// Print the squares
for(int i=0;i<size;++i){
std::cout << squares[i] << " ";
}
std::cout << std::endl;
return 0;
}
Output:
![C++ Concurrency in action](https://static.tutorialandexample.com/cpp/cpp-concurrency-in-action6.png)
Explanation:
The loop that calculates the squares is parallelized in this program using the #pragma omp parallel for directive. This instruction instructs the compiler to divide the loop's iterations among several threads.
The calculation is carried out by each thread for a portion of the loop iterations, which boosts efficiency.
7. Synchronization:
The practice of synchronizing access to shared resources, like as memory or files, amongst many threads or processes is known as synchronization. It is crucial to prevent race situations or other mistakes from occurring as a result of concurrent access to shared resources.
Using mutexes, conditional variables, or atomic operations, synchronization can be accomplished in C++.
Program:[i]
#include<thread>
#include <mutex>
std::mutex mtx;
void printMessage(const std::string&message,int delay) {
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
std::lock_guard<std::mutex> lock(mtx); // Lock the mutex
// Critical section
std::cout << message << std::endl;
}
int main(){
std::thread t1(printMessage, "Hello", 200); // Create thread t1
std::thread t2(printMessage, "World", 100); // Create thread t2
// Wait for both threads to finish
t1.join();
t2.join();
return 0;
}
Output:
![C++ Concurrency in action](https://static.tutorialandexample.com/cpp/cpp-concurrency-in-action7.png)
Explanation:
The printMessage function in this program accepts two parameters: a message and a delay. After sleeping for the designated amount of time, it hits a critical area where a std::lock_guard is used to lock the mutex. The message is simply printed to the console in the vital region.
We construct two threads, t1 and t2, in the main function, which call the printMessage function with various messages and delays. Before the program terminates, we use join to wait for both threads to finish.
We prohibit interleaved output and guarantee thread synchronization by using the mutex to ensure that only one thread can access the important portion at a time.
8. Concurrency Patterns:
Synchronization, communication, and resource management are just a few of the main design problems that concurrency patterns are used to solve in concurrent programming.
The reader-writer pattern, the monitor pattern, and the producer-consumer pattern are a few examples of concurrency patterns in C++.
Program:
#include<iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
std::queue<int> sharedQueue;
std::mutex mtx;
std::condition_variable cv;
void producer()
{
for(int i=1;i<11;++i)
{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::unique_lock<std::mutex> lock(mtx);
sharedQueue.push(i);
std::cout << "Produced: " << i << std::endl;
lock.unlock();
cv.notify_one(); // Notify consumer
}
}
void consumer()
{
while(true)
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !sharedQueue.empty(); });
int data = sharedQueue.front();
sharedQueue.pop();
lock.unlock();
std::cout << "Consumed: " << data << std::endl;
if (data == 10)
break; // Stop consuming after reaching 10
}
}
int main()
{
std::thread producerThread(producer);
std::thread consumerThread(consumer);
producerThread.join();
consumerThread.join();
return 0;
}
Output:
![C++ Concurrency in action](https://static.tutorialandexample.com/cpp/cpp-concurrency-in-action8.png)
Explanation:
The producer function in this program generates values between 1 and 10 and adds them to the shared queue. The consumer function prints the values it has taken from the queue. The producer and consumer functions are both run simultaneously in different threads.
The shared queue is secured by the std::mutex (mtx), which makes sure that only one thread may edit it concurrently. The producer and consumer threads are synchronized using the std::condition_variable (cv).
The producer thread notifies the consumer thread when it has created a value, and the consumer thread waits until there is data available in the queue.
The producer function uses the std::this_thread::sleep_for function to simulate work being done before providing the next value.
Best Practices:
When working with concurrency in C++, it's important to follow best practices to ensure correct and efficient behaviour of your multithreaded code. Here are some best practices for C++ concurrency:
Minimize shared mutable state:
Shared mutable state can introduce race conditions and make code difficult to reason about. Minimize the amount of shared data between threads as much as possible. If sharing is necessary, encapsulate shared state within a class and protect it with appropriate synchronization mechanisms.
Use synchronization mechanisms correctly: Properly synchronize access to shared data using synchronization primitives like mutexes, locks, and condition variables. Acquire locks for the minimum possible time to reduce contention and improve performance. Avoid holding multiple locks simultaneously to prevent deadlocks.
Avoid unnecessary contention: Minimize contention between threads by reducing the frequency and duration of lock acquisitions. This can be achieved by partitioning data or operations to reduce the need for shared access, employing lock-free techniques where applicable, and using fine-grained locking strategies.
Be aware of thread-safety guarantees: Understand the thread-safety guarantees of the libraries, frameworks, and third-party code you use. Ensure that you adhere to those guarantees and use proper synchronization if needed when interacting with thread-unsafe components.
Use thread pools or task-based parallelism: Instead of creating threads manually for each task, consider using thread pools or task-based parallelism frameworks like Intel TBB or OpenMP. These abstractions can manage the thread lifecycle and workload distribution efficiently, simplifying the development of concurrent applications.
Conclusion
Threads, mutexes, condition variables, and futures are just a few of the concurrency programming capabilities and packages that C++ offers. The performance and responsiveness of apps that entail CPU- or I/O-bound operations can be enhanced by using these characteristics.
Concurrency programming may be challenging and error-prone, therefore it's crucial to use these capabilities effectively and safely. However, C++ may be a potent tool for concurrent programming with careful design and testing.