C++ stands out as a robust and adaptable programming language. It accommodates various programming paradigms, one of which is concurrency. Concurrency refers to the capability of running multiple threads of execution concurrently within a program. This practice enhances performance and responsiveness, particularly in applications dealing with I/O-bound or CPU-bound operations. C++ offers inherent functionalities and libraries tailored for concurrent programming, like threads, mutexes, condition variables, and futures.
1. Threads
It is a series of commands that can run separately from the primary program. In C++, you can generate a thread using the std::thread class from the standard library. This concept can be illustrated with the following example.
Example 1:
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello, world!" << std::endl;
}
int main() {
std::thread t(hello);
t.join();
return 0;
}
Output:
Explanation
In this instance, a fresh thread is generated by providing the "hello" function as an argument to the std::thread constructor. Subsequently, the join function is invoked on the thread instance to ensure the thread completes its execution prior to program termination.
2. Mutexes
A mutex serves as a synchronization mechanism to safeguard shared resources from simultaneous access by multiple threads. In C++, you can establish a mutex by utilizing the std::mutex class provided in the standard library. This concept can be illustrated through the following example.
Example 2:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex m;
void hello() {
m.lock();
std::cout << "Hello, world!" << std::endl;
m.unlock();
}
int main() {
std::thread t(hello);
t.join();
return 0;
}
Output:
Explanation
In this instance, a mutex is established with the assistance of the std::mutex class. The acquire function is invoked on the mutex instance prior to interacting with the shared resource (in this scenario, the standard output stream). Subsequently, The release function is invoked to relinquish the mutex once the shared resource has been utilized.
3. Condition Variables
A condition variable serves as a synchronization mechanism that halts a thread until a specific condition becomes true. In C++, you can establish a condition variable by utilizing the std::condition_variable class provided in the standard library. Let's illustrate this through the following example.
Example 3:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex m;
std::condition_variable cv;
void hello() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock);
std::cout << "Hello, world!" << std::endl;
}
int main() {
std::thread t(hello);
std::this_thread::sleep_for(std::chrono::seconds(1));
cv.notify_one();
t.join();
return 0;
}
Output:
Explanation:
In this instance, a condition variable is instantiated using the std::conditionvariable class. Executing the wait function on the condition variable halts the thread until a notification is received from another thread. Subsequently, invoking the notifyone function notifies the blocked thread that the condition is met. The std::unique_lock<std::mutex> is employed to automatically manage locking and unlocking of the mutex object.
4. Futures
It serves as a synchronization tool that allows fetching a value from an executing thread or function that operates asynchronously. In C++, an instance of a future can be generated with the support of the std::future class provided by the standard library. Let's illustrate this concept with the following example.
Example 4:
#include <iostream>
#include <thread>
#include <future>
int hello() {
return 42;
}
int main() {
std::future<int> f = std::async(std::launch::async, hello);
std::cout << "Answer: " << f.get() << std::endl;
return 0;
}
Output:
Explanation
In this instance, a future is generated with the assistance of the std::future class. The std::async function is invoked to initiate a new thread and run the hello function concurrently. Subsequently, the get function is utilized on the future instance to obtain the output of the hello function.
5. Multithreading
It is the capacity to run multiple code threads simultaneously within a single program. Threads represent distinct flows of execution that can operate concurrently. This functionality is beneficial for software that needs parallel processing, like video encoding, scientific simulations, or graphic rendering. In C++, implementing multithreading is possible using the thread class from the Standard Template Library (STL).
Example of Multithreading:
#include <iostream>
#include <thread>
void foo() {
std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
}
int main() {
std::thread t1(foo);
std::thread t2(foo);
t1.join();
t2.join();
return 0;
}
Output:
Explanation
In the example provided, two threads are instantiated by utilizing the std::thread class to concurrently execute the foo function. Prior to program termination, the join method is invoked on each thread to ensure they complete their execution.
6. Parallelism:
The capability of carrying out numerous tasks concurrently through multiple threads or processes is known as parallelism. This feature proves beneficial for software applications that demand superior performance and effectiveness, like data processing, scientific simulations, or machine learning tasks. Within C++, parallelism can be accomplished by leveraging either the Parallel STL library or OpenMP.
Example of Parallelism:
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
int main() {
std::vector<int> v {1, 5, 3, 2, 4};
std::for_each(std::execution::par, v.begin(), v.end(), [](int& i) {
std::cout << "Thread " << std::this_thread::get_id() << " processing " << i << std::endl;
i *= 2;
});
for (int i : v) {
std::cout << i << " ";
}
return 0;
}
Output:
Explanation:
In the example provided, the std::for_each algorithm is employed to apply a lambda expression to every item in a vector simultaneously utilizing the std::execution::par policy. The lambda expression outputs the thread ID and carries out the task of doubling the value of each element.
7. Synchronization:
It involves managing access to shared resources, like memory or files, among multiple threads or processes. Ensuring that simultaneous access to shared resources does not lead to race conditions or errors is crucial. In C++, synchronization can be accomplished through mutexes, condition variables, or atomic operations.
Example of Synchronization:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex m;
void foo() {
m.lock();
std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
m.unlock();
}
int main() {
std::thread t1(foo);
std::thread t2(foo);
t1.join();
t2.join();
return 0;
}
Output:
Explanation
In the scenario described, a mutex is employed to coordinate access to the std::cout object, serving as a shared entity between two threads that are running the foo function. Through the utilization of the lock and unlock functions, we can obtain and release the mutex as needed.
8. Concurrency Patterns:
Concurrency patterns are frequently utilized design patterns in concurrent programming to address typical challenges like synchronization, communication, and resource management. Within C++, instances of concurrency patterns encompass the producer-consumer pattern, the reader-writer pattern, and the monitor pattern.
Example of Concurrency Pattern:
#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
std::mutex m;
std::queue<int> q;
std::condition_variable cv;
void producer() {
for (int i = 0; i < 5; ++i) {
std::unique_lockstd::mutex lock(m);
q.push(i);
std::cout << "Produced " << i << std::endl; lock.unlock();
cv.notify_one(); std::this_thread::sleep_for(std::chrono::milliseconds(500)); } }
void consumer() {
while (true) {
std::unique_lockstd::mutex lock(m);
cv.wait(lock, []{ return !q.empty(); });
int value = q.front();
q.pop();
std::cout << "Consumed " << value << std::endl;
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
Output:
Explanation
In the above example, a simple producer-consumer pattern is implemented using a mutex, a condition variable, and a shared queue. The producer function generates five integer values and pushes them onto the queue with a delay of 500 milliseconds between each push. After each push, it notifies the waiting consumer thread using the notify_one method. The consumer function waits for notifications from the producer thread using the wait method of the condition variable. It consumes the values from the queue with a delay of 1000 milliseconds between each consumption.
Best Practices
When dealing with concurrency in C++, it's crucial to adhere to recommended guidelines to guarantee the dependability and effectiveness of your program. These guidelines involve steering clear of shared mutable state, employing thread-safe data structures, reducing reliance on locks and synchronization, and conducting comprehensive testing of your code.
Conclusion
C++ offers an extensive array of functionalities and libraries for concurrent programming, including threads, locks, condition variables, and promises. These functionalities are beneficial for enhancing the efficiency and interactivity of applications handling tasks related to input/output operations or CPU processing. Proper and secure utilization of these functionalities is crucial due to the intricacies and potential pitfalls of concurrent programming. Nevertheless, with meticulous planning and thorough testing, C++ can serve as a robust resource for concurrent programming tasks.