C++ is a powerful and versatile programming language. It supports a wide range of programming paradigms, including concurrency. Concurrency is the ability to execute multiple threads of execution simultaneously in a program. It results in improved performance and responsiveness, especially in applications that involve I/O-bound or CPU-bound tasks. C++ provides built-in features and libraries for concurrency programming, such as threads, mutexes, condition variables, and futures.
1. Threads
It is a sequence of instructions that can be executed independently of the main program. In C++, the thread can be created using the std::thread class from the standard library. Let's understand this by the below 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 example, a new thread is created by passing the" hello" function as a parameter to the std::thread constructor. Then The join method is called on the thread object to wait for the thread to finish before exiting the program.
2. Mutexes
A mutex is a synchronization object that can be used to protect shared resources from concurrent access by multiple threads. In C++, a mutex can be created using the help of std::mutex class from the standard library. Let's understand this by the below 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 example, a mutex is created using the help of std::mutex class. The lock method is called on the mutex object before accessing the shared resource (in this case, the standard output stream). Then The unlock method is called to release the mutex after the shared resource has been accessed.
3. Condition Variables
A condition variable is a synchronization object that blocks a thread until a particular condition is satisfied. In C++, a condition variable can be created using the help of std::condition_variable class from the standard library. Let's understand this by the below 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 example, a condition variable is created using the std::conditionvariable class. The wait method is called on the condition variable to block the thread until it is notified by another thread. Then The notifyone method is called to inform the waiting thread that the condition has been satisfied. The std::unique_lock<std::mutex> is used to lock and unlock the mutex object automatically.
4. Futures
It is a synchronization object that can be used to retrieve a value from a thread or function that is executing asynchronously. In C++, a future can be created using the help of std::future class from the standard library. Let's understand this by the below 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 example, a future is created using the help of the std::future class. The std::async function is called to launch a new thread and execute the hello function asynchronously. Then The get method is called on the future object to retrieve the return value of the hello function.
5. Multithreading
It is the ability to execute multiple code threads within a single application. Threads are independent paths of execution that can run concurrently with each other. It is helpful for applications that require parallelisms, such as video encoding, scientific simulations, or graphics rendering. In C++, multithreading can be achieved through the help of the Standard Template Library (STL) thread class.
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 above example, two threads are created using the help of std::thread class to execute the foo function concurrently. Before exiting the program, The join method is called on both threads to wait for them to finish.
6. Parallelism:
It is the ability to execute multiple tasks simultaneously using multiple threads or processes. Parallelism is helpful for applications that require high performance and efficiency, such as data processing, scientific simulations, or machine learning. In C++, parallelism can be achieved using 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 above example, the std::for_each algorithm is used to apply a lambda function to each element of a vector in Parallel using the std::execution::par policy. The lambda function simply prints the current thread ID and processes the element by doubling its value.
7. Synchronization:
It is the process of coordinating access to shared resources, such as memory or files, between multiple threads or processes. It is important to ensure that concurrent access to shared resources does not result in race conditions or other errors. In C++, synchronization can be achieved using 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 above example, a mutex is used to synchronize access to the std::cout object, which is a shared resource between two threads executing the foo function. With The help of lock and unlock methods, we can acquire and release the mutex.
8. Concurrency Patterns:
Concurrency patterns are common design patterns used in concurrent programming to solve common problems, such as synchronization, communication, and resource management. in C++, Examples of concurrency patterns include 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 working with concurrency in C++, it is essential to follow best practices to ensure the reliability and efficiency of your application. Best practices include avoiding the shared mutable state, using thread-safe data structures, minimizing the use of locks and synchronization, and testing your code thoroughly.
Conclusion
C++ provides a wide range of features and libraries for concurrency programming, such as threads, mutexes, condition variables, and futures. These features can be used to improve the performance and responsiveness of applications that involve I/O-bound or CPU-bound tasks. It is important to use these features correctly and safely, as concurrency programming can be complex and error-prone. With careful design and testing, however, C++ can be a powerful tool for concurrent programming.