Back to C++
2025-12-286 min read

C++ Concurrency

Learn C++ Concurrency step by step with clear examples and exercises.

Title: Mastering C++ Concurrency: A full guide for Modern Programming

Why This Matters

In today's fast-paced world, efficient and effective concurrent programming is crucial to building high-performance applications. C++ provides robust support for concurrent programming through threads, synchronization primitives, and standard libraries. Understanding these concepts can help you write scalable, responsive, and reliable software that can handle multiple tasks simultaneously. This lesson will guide you through the core principles of C++ concurrency, providing practical examples, common pitfalls to avoid, and answers to frequently asked questions.

Prerequisites

To fully grasp this lesson, you should have a solid understanding of:

  • Basic C++ syntax and control structures (loops, conditionals)
  • Data structures like arrays, vectors, and linked lists
  • Object-oriented programming concepts in C++ (classes, objects, inheritance)
  • Basic file I/O operations
  • Familiarity with the Standard Template Library (STL)
  • Exception handling to manage errors gracefully
  • Memory management techniques, such as dynamic memory allocation and deallocation
  • Resource Acquisition Is Initialization (RAII) principles for managing resources efficiently

Important Concepts to Review:

  • Exception Handling
  • Memory Management
  • Resource Acquisition Is Initialization (RAII)

Core Concept

Understanding Threads

A thread is a separate sequence of instructions that can run concurrently with other threads. In C++, you can create and manage threads using the std::thread library.

#include <thread>
#include <iostream>

void printHello() {
std::cout << "Hello from a thread!\n";
}

int main() {
std::thread t(printHello); // create and start a new thread
t.join(); // wait for the thread to finish before exiting main
return 0;
}

Synchronization Primitives

Synchronization primitives help manage concurrent access to shared resources, ensuring that threads execute in a predictable and safe manner.

Mutexes

Mutual exclusion locks used to protect critical sections of code from simultaneous execution by multiple threads.

#include <mutex>
#include <iostream>
#include <vector>

std::mutex mtx; // create a mutex object
std::vector<int> numbers;

void addNumber(int n) {
std::lock_guard<std::mutex> lock(mtx); // acquire the lock before accessing shared data
numbers.push_back(n);
}

Condition Variables

Used to block a thread until a specific condition is met, allowing for efficient synchronization between threads.

#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <iostream>

std::mutex mtx; // create a mutex object
std::condition_variable cv; // create a condition variable object
std::queue<int> numbers;
bool done = false;

void producer(int num_elements) {
for (int i = 0; i < num_elements; ++i) {
std::lock_guard<std::mutex> lock(mtx); // acquire the lock before accessing shared data
numbers.push(i);
if (numbers.size() == num_elements) {
done = true;
cv.notify_one(); // notify one waiting consumer thread
}
}
}

void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !numbers.empty() || done; }); // wait until there are numbers to consume or all numbers have been produced
if (!numbers.empty()) {
int num = numbers.front();
numbers.pop();
std::cout << "Consumed: " << num << "\n";
}
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main() {
const int num_elements = 10;
std::thread producerThread(producer, num_elements); // create a producer thread
std::thread consumerThread(consumer); // create a consumer thread
producerThread.join();
consumerThread.join();
return 0;
}

Atomics and Memory Ordering

Atomic operations ensure that individual memory operations (e.g., increment, decrement) are executed atomically, preventing race conditions. C++ provides atomic types in the `` header to simplify the use of atomic variables.

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter(0);

void increment() {
for (int i = 0; i < 1000000; ++i) {
counter++;
}
}

int main() {
const int num_threads = 4;
std::vector<std::thread> threads(num_threads);

for (size_t i = 0; i < num_threads; ++i) {
threads[i] = std::thread(increment);
}

for (auto& t : threads) {
t.join();
}

std::cout << "Final counter value: " << counter << "\n";
return 0;
}

Worked Example

Implement a simple concurrent web server using the Boost.Asio library, which provides a powerful set of tools for network programming in C++.

#include <boost/asio.hpp>
#include <iostream>
#include <thread>
#include <vector>

namespace asio = boost::asio;

void handle_request(const asio::ip::tcp::socket& socket) {
// read request from client
char buffer[1024];
asio::streambuf request;
asio::read_until(socket, request, "\r\n");
std::istream is(&request);
is >> buffer;

// process request and send response
std::cout << "Received request: " << buffer << "\n";
asio::streambuf response;
std::ostream os(&response);
os << "HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\nHello, world!\n";
asio::write(socket, response);
}

int main() {
try {
asio::io_context io_context; // create an I/O context object
asio::ip::tcp::acceptor acceptor(io_context, asio::ip::tcp::endpoint(asio::ip::make_address("0.0.0.0", 8080), std::make_tuple())); // create an acceptor on port 8080

while (true) {
asio::ip::tcp::socket socket = acceptor.accept(); // accept a new connection
std::thread thread(handle_request, std::move(socket)); // start a new thread to handle the request
thread.detach(); // detach the thread from the current process (don't join)
}
} catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
}

return 0;
}

Common Mistakes

Failing to Synchronize Access to Shared Resources

When multiple threads access a shared resource without proper synchronization, race conditions can occur, leading to unpredictable and incorrect behavior.

Example of Race Condition:

#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

std::atomic<int> counter(0);
std::vector<int> numbers;
const int num_elements = 10;

void producer() {
for (int i = 0; i < num_elements; ++i) {
numbers.push_back(i);
counter++;
}
}

void consumer() {
while (counter.load() != num_elements) {
// do nothing
}

for (const auto& number : numbers) {
std::cout << "Consumed: " << number << "\n";
}
}

int main() {
std::thread producerThread(producer);
std::thread consumerThread(consumer);

producerThread.join();
consumerThread.join();

return 0;
}

In the above example, the consumer thread might consume fewer than num_elements numbers because it checks the counter after the producer has incremented it but before all elements have been produced. This can lead to incorrect results or race conditions.

Not Properly Joining Threads

If you forget to join threads in the main function before exiting, your program may terminate prematurely, leaving some threads running indefinitely or causing resource leaks.

Example of Unjoined Thread:

#include <iostream>
#include <thread>

void printHello() {
std::cout << "Hello from a thread!\n";
}

int main() {
std::thread t(printHello); // create and start a new thread
// forget to join the thread
return 0;
}

In the above example, the program will terminate before the printHello function finishes executing. This can lead to unpredictable behavior or memory leaks if the thread performs important tasks like resource allocation or file I/O operations.

Ignoring Exception Handling

In concurrent programming, exceptions can occur at any time due to various reasons (e.g., network errors, thread termination). It's essential to handle exceptions properly to ensure the program's stability and graceful shutdown.

Practice Questions

  1. Write a simple program that creates 10 threads, each printing its own ID number from 1 to 10 using std::this_thread::get_id().
  2. Implement a concurrent producer-consumer problem using mutexes and condition variables, where the producer generates random numbers between 1 and 100 and the consumer finds the maximum value produced so far.
  3. Create a multi-threaded web crawler that downloads HTML pages from a list of URLs and saves their content to separate files. Use Boost.Asio for network operations and std::filesystem for file I/O.

FAQ

Q: What is the difference between a thread and a process?

A: A process represents an executing program, while a thread is a lightweight sequence of instructions within a process. Processes have their own memory space, while threads share the same memory space as the parent process.

Q: How do I handle exceptions in concurrent programming?

A: To handle exceptions in concurrent programming, you can use exception specifications (e.g., throw() and noexcept) to indicate which functions are exception-safe and don't throw exceptions. You can also use RAII (Resource Acquisition Is Initialization) techniques to ensure that resources are properly cleaned up even in the presence of exceptions.

Q: What is the Boost.Asio library, and why should I use it for network programming?

A: The Boost.Asio library is a powerful C++ library for network programming that provides a consistent interface for working with sockets, timers, and other I/O operations on various platforms. It abstracts away the complexities of platform-specific APIs, making it easier to write portable and efficient network applications in C++.

C++ Concurrency | C++ | XQA Learn