C++ Multithreading
C++ Multithreading using the Standard Library
Section titled “C++ Multithreading using the Standard Library”Multithreading allows a program to perform multiple tasks concurrently, improving performance and responsiveness, especially on multi-core processors. Modern C++ provides robust support for multithreading directly within the Standard Library (since C++11), primarily through headers like <thread>, <mutex>, <condition_variable>, and <future>.
While operating system-specific APIs like POSIX Threads (pthreads) or Windows threads exist and are sometimes used for platform-specific features, the C++ Standard Library offers a portable and type-safe way to manage threads and synchronization.
Key Concepts:
- Thread: An independent path of execution within a process.
- Concurrency: Multiple threads making progress over time (may involve switching on a single core).
- Parallelism: Multiple threads executing simultaneously on different cores.
- Synchronization: Mechanisms (like mutexes, condition variables) to coordinate access to shared resources between threads and prevent data races.
Creating Threads with std::thread
Section titled “Creating Threads with std::thread”The <thread> header provides the std::thread class to create and manage threads. You create a thread by constructing a std::thread object, passing a callable entity (function, lambda expression, functor) and its arguments.
#include <iostream>#include <thread> // Required for std::thread#include <vector>#include <string>
// Function to be executed by the threadvoid print_message(int id, const std::string& message) { // std::this_thread::get_id() returns the unique ID of the current thread std::cout << "Thread [" << std::this_thread::get_id() << "] ID: " << id << " says: " << message << std::endl;}
int main() { std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl;
// Create thread t1 executing print_message(1, "Hello from thread 1") std::thread t1(print_message, 1, "Hello from thread 1");
// Create thread t2 using a lambda expression int thread_id_2 = 2; std::thread t2([thread_id_2]() { // Capture thread_id_2 by value std::cout << "Thread [" << std::this_thread::get_id() << "] ID: " << thread_id_2 << " says: Hi from lambda!" << std::endl; });
// Create multiple threads std::vector<std::thread> threads; for (int i = 3; i <= 5; ++i) { // Use emplace_back to construct threads directly in the vector threads.emplace_back(print_message, i, "Greetings from thread " + std::to_string(i)); }
std::cout << "Main thread continues execution..." << std::endl;
// Wait for threads to complete (Join) std::cout << "Waiting for threads to finish..." << std::endl; t1.join(); t2.join(); for (auto& t : threads) { t.join(); }
std::cout << "All threads have completed." << std::endl; return 0;}Compile using a C++11 (or later) compliant compiler, linking the thread library if necessary (often -pthread or -lpthread flag for GCC/Clang):
g++ your_file.cpp -o thread_example -std=c++11 -pthreadThe output order of messages from different threads is non-deterministic due to concurrent execution.
Joining and Detaching Threads
Section titled “Joining and Detaching Threads”A std::thread object must be either joined or detached before it goes out of scope or is destroyed. Failure to do so will result in std::terminate being called.
join(): Causes the calling thread (e.g., the main thread) to block and wait until the associated thread (t1,t2, etc.) finishes execution. This is the most common way to manage thread lifetime.detach(): Separates the thread of execution from thestd::threadobject. The thread continues execution independently in the background. The originalstd::threadobject no longer represents the running thread and cannot be joined. Detached threads must manage their own lifetime carefully; ifmainexits, detached threads are typically terminated abruptly. Usedetach()with caution.
// Example of detaching (use carefully!)// std::thread t3(background_task);// t3.detach(); // t3 object no longer manages the thread// // Main thread continues without waiting for background_taskPassing Arguments
Section titled “Passing Arguments”Arguments are passed to the thread function by copying or moving them during std::thread construction. Be mindful of argument types:
- By Value: Arguments are copied. Changes inside the thread function do not affect original variables.
- By Reference: To pass arguments by reference, wrap them in
std::ref()orstd::cref()(for const reference). - By Pointer: Pointers can be passed directly, but ensure the pointed-to object remains valid for the thread’s lifetime (beware of dangling pointers).
- Move Semantics: Objects supporting move semantics can be moved into a thread, transferring ownership.
#include <iostream>#include <thread>#include <string>#include <functional> // for std::ref, std::cref
void modify_value(int& value) { // Takes int by reference value *= 2; std::cout << "Thread modified value to: " << value << std::endl;}
void read_value(const int& value) { // Takes int by const reference std::cout << "Thread read value: " << value << std::endl;}
int main() { int shared_data = 10;
// Pass by reference using std::ref std::thread t_modify(modify_value, std::ref(shared_data)); t_modify.join(); // Wait for modification std::cout << "Main thread sees shared_data as: " << shared_data << std::endl; // Should be 20
// Pass by const reference using std::cref or std::ref std::thread t_read(read_value, std::cref(shared_data)); t_read.join();
return 0;}Synchronization: Mutexes
Section titled “Synchronization: Mutexes”When multiple threads access shared data, and at least one thread modifies it, you need synchronization to prevent data races (undefined behavior). A mutex (Mutual Exclusion) is the most basic synchronization primitive.
The <mutex> header provides std::mutex. A thread locks the mutex before accessing shared data and unlocks it afterward. Only one thread can hold the lock at any given time.
Using RAII wrappers like std::lock_guard or std::scoped_lock (C++17) is highly recommended. They automatically lock the mutex on construction and unlock it on destruction (when they go out of scope), ensuring the mutex is always released, even if exceptions occur.
#include <iostream>#include <thread>#include <vector>#include <mutex> // Required for std::mutex and std::lock_guard
std::mutex cout_mutex; // Mutex to protect access to std::coutint shared_counter = 0;std::mutex counter_mutex; // Mutex to protect access to shared_counter
void increment_counter(int id) { for (int i = 0; i < 5; ++i) { // Lock the counter mutex before accessing shared_counter // std::lock_guard automatically unlocks when 'lock' goes out of scope std::lock_guard<std::mutex> lock(counter_mutex); shared_counter++;
// Lock the cout mutex before printing { std::lock_guard<std::mutex> cout_lock(cout_mutex); std::cout << "Thread " << id << " incremented counter to " << shared_counter << std::endl; } // cout_lock is released here
// Simulate some work std::this_thread::sleep_for(std::chrono::milliseconds(10));
} // lock (for counter_mutex) is released here}
int main() { std::vector<std::thread> threads; for (int i = 0; i < 3; ++i) { threads.emplace_back(increment_counter, i + 1); }
for (auto& t : threads) { t.join(); }
std::cout << "Final counter value: " << shared_counter << std::endl; // Should be 15 return 0;}Other Synchronization Primitives
Section titled “Other Synchronization Primitives”The C++ Standard Library provides other tools for more complex synchronization scenarios:
std::condition_variable: Allows threads to wait efficiently for a specific condition to become true.std::atomic: Provides atomic operations (indivisible, uninterruptible) on basic types, often more efficient than mutexes for simple counters or flags.std::futureandstd::promise/std::async: Enable retrieving results from asynchronous operations (tasks running potentially in other threads).
Multithreading introduces complexity (debugging, potential deadlocks, livelocks). Start simple, use RAII for resource management (locks), and carefully design interactions between threads.