This chapter delves into the exciting world of concurrency and parallelism in C++. We'll explore how to make your C++ programs handle multiple tasks seemingly "at the same time," improving responsiveness and performance.
Concurrency refers to the ability of a program to handle multiple tasks (processes or threads) seemingly at the same time. This creates the illusion of multitasking, even on a single CPU core. The key here is the “illusion.” The CPU can only truly execute one instruction at a time. However, by rapidly switching between tasks, concurrency makes it appear as if multiple tasks are running simultaneously.
Parallelism refers to the actual execution of multiple tasks simultaneously. This requires a system with multiple processing units (cores) that can truly execute instructions concurrently. When multiple cores are available, parallelism leverages them to genuinely perform tasks in parallel, achieving significant performance improvements.
Here’s an analogy: Imagine juggling. Concurrency is like keeping multiple balls in the air by throwing and catching them one after another very quickly. It creates the illusion of multiple balls being airborne simultaneously. Parallelism, on the other hand, is like having multiple hands and juggling multiple balls genuinely at the same time.
Threads are lightweight units of execution within a process. A process is an instance of a program running on the system. A single process can have multiple threads of execution, each with its own call stack and program counter.
The C++ standard library provides mechanisms for creating and managing threads. Here’s a basic example using the <thread>
header (C++11 and later):
#include
#include
void printNumber(int number) {
for (int i = 0; i < 5; ++i) {
std::cout << "Thread " << number << ": " << i << std::endl;
}
}
int main() {
// Create a thread object
std::thread first_thread(printNumber, 1);
// The main thread also prints numbers
for (int i = 5; i < 10; ++i) {
std::cout << "Main thread: " << i << std::endl;
}
// Wait for the thread to finish
first_thread.join();
return 0;
}
// output //
Main thread: 5
Main thread: 6
Thread 1: 0
Thread 1: 1
Main thread: 7
Thread 1: 2
Thread 1: 3
Main thread: 8
Thread 1: 4
Main thread: 9
<iostream>
for input/output and <thread>
for thread management.printNumber
that prints a sequence of numbers.main
, we create a std::thread
object first_thread
that executes the printNumber
function with argument 1
.main
thread continues printing numbers (5 to 9).first_thread.join()
to wait for the first_thread
to finish execution before continuing in main
.This is a simple example of creating and joining a thread. In practice, threads are used for more complex tasks that can run concurrently with the main thread.
When multiple threads access and modify the same data (shared data), there’s a risk of data races and inconsistencies. A data race occurs when multiple threads access the same data location without proper synchronization, leading to unpredictable program behavior.
C++ provides synchronization primitives (like mutexes) to ensure safe access to shared data. These primitives act like locks or gates that control access to shared data. Here are some common synchronization primitives:
Mutex (Mutual Exclusion): A mutex object allows only one thread to acquire the lock (ownership) at a time. Other threads attempting to acquire the lock will be blocked until the current owner releases it. This ensures exclusive access to a shared resource.
Condition Variables: Used in conjunction with mutexes. A thread can wait on a condition variable while holding the mutex lock. Another thread can signal the condition variable, allowing the waiting thread to proceed when a specific condition is met.
Semaphores: Act as a counter that controls access to a limited number of resources. A thread attempting to acquire a semaphore when the counter is zero will be blocked until another thread releases a resource (increments the counter).
#include
#include
#include
int counter = 0;
std::mutex mtx;
void incrementCounter() {
// Acquire the mutex lock before accessing the counter
mtx.lock();
counter++;
mtx.unlock();
}
int main() {
// Create multiple threads
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(incrementCounter);
}
// Wait for all threads to finish
for (auto& thread : threads) {
thread.join();
}
std::cout << "Final counter value: " << counter << std::endl;
// Expected output: Final counter value: 5 (assuming proper synchronization)
}
counter
variable and a mutex
object mtx
.incrementCounter
function acquires the mutex lock before incrementing the counter.main
, we create multiple threads that call incrementCounter
.join
calls ensure all threads finish before printing the final counter value.The choice of synchronization primitive depends on your specific needs:
Asynchronous programming involves launching tasks (often without creating separate threads) and receiving notifications when they complete. This approach can improve responsiveness and avoid blocking the main thread for long-running operations. The <future>
header (C++11 and later) provides mechanisms for asynchronous programming.
Modern C++ libraries like the C++20 Executors framework provide higher-level abstractions for managing concurrent tasks and scheduling them on available threads or cores. This simplifies concurrency management and offers better resource utilization.
The C++ Standard Library provides parallel algorithms (like std::transform
, std::for_each
) that leverage multiple cores to perform operations on data structures in parallel. These algorithms can significantly improve performance for CPU-bound tasks that can be efficiently parallelized.
std::vector
with const
iterators) to avoid the need for synchronization for read-only access.By effectively using threads, synchronization primitives, and advanced concurrency models, you can design C++ programs that handle multiple tasks efficiently, improving responsiveness and performance. Remember to start with simple concurrency concepts and gradually progress to more advanced techniques as your understanding grows.Happy coding !❤️