In C++, multithreading enables the concurrent execution of multiple tasks or parts of a program, allowing for efficient utilization of system resources, particularly on multi-core systems.
C++ provides a threading library in <thread> which makes it easy to create and manage threads.
Along with <mutex> and <condition_variable>, you can coordinate between threads and handle data sharing safely.
Basic Multithreading Concepts in C++
- Thread Creation: Creating multiple threads to execute tasks concurrently.
- Thread Joining and Detaching: Managing thread execution by joining or detaching them.
- Mutex: Prevents data races by allowing only one thread to access shared data at a time.
- Condition Variables: Synchronize threads by blocking or notifying them.
- Atomic Operations: Perform thread-safe operations without requiring a mutex.
1. Basic Thread Creation and Execution
In this example, we create a simple thread to execute a task concurrently.
#include <iostream> #include <thread> using namespace std; void printMessage() { cout << "Hello from thread!" << endl; } int main() { thread t(printMessage); // Create a thread to run printMessage t.join(); // Wait for the thread to finish cout << "Main thread ends." << endl; return 0; }
Explanation:
- thread t(printMessage); creates a thread t that executes printMessage.
- t.join(); ensures the main thread waits for t to complete before continuing.
Output:
Hello from thread! Main thread ends.
2. Passing Arguments to Threads
You can pass arguments to thread functions using the std::thread constructor.
#include <iostream> #include <thread> using namespace std; void addNumbers(int a, int b) { cout << "Sum: " << a + b << endl; } int main() { thread t(addNumbers, 5, 3); // Pass 5 and 3 as arguments to addNumbers t.join(); return 0; }
Explanation:
- thread t(addNumbers, 5, 3); creates a thread that calls addNumbers(5, 3).
Output:
Sum: 8
3. Lambda Functions in Threads
Lambda expressions provide an inline way to define thread functions.
#include <iostream> #include <thread> using namespace std; int main() { int x = 10, y = 5; thread t([x, y]() { cout << "Difference: " << x - y << endl; }); t.join(); return 0; }
Explanation:
- thread t([x, y]() { … }); creates a thread with an inline lambda function.
Output:
Difference: 5
4. Detaching Threads
A detached thread runs independently of the main thread and does not need to be joined.
#include <iostream> #include <thread> #include <chrono> using namespace std; void printMessage() { this_thread::sleep_for(chrono::seconds(2)); cout << "Hello from detached thread!" << endl; } int main() { thread t(printMessage); t.detach(); // Detach the thread, allowing it to run independently cout << "Main thread ends before detached thread." << endl; // Adding a small sleep to ensure detached thread prints before program exit this_thread::sleep_for(chrono::seconds(3)); return 0; }
Explanation:
- t.detach(); detaches the thread, allowing it to run independently of the main thread.
Output:
Main thread ends before detached thread. Hello from detached thread!
5. Using Mutex for Thread Synchronization
Mutexes prevent data races by allowing only one thread to access shared data at a time.
#include <iostream> #include <thread> #include <mutex> using namespace std; mutex mtx; void printNumber(int n) { lock_guard<mutex> lock(mtx); // Locks the mutex until the end of scope cout << "Number: " << n << endl; } int main() { thread t1(printNumber, 1); thread t2(printNumber, 2); t1.join(); t2.join(); return 0; }
Explanation:
- lock_guard<mutex> lock(mtx); locks mtx to ensure only one thread accesses cout at a time.
Output (order may vary):
Number: 1 Number: 2
6. Using std::unique_lock and Condition Variables
Condition variables allow threads to wait for a condition to be met before continuing.
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> using namespace std; mutex mtx; condition_variable cv; bool ready = false; void printMessage() { unique_lock<mutex> lock(mtx); cv.wait(lock, [] { return ready; }); // Wait until ready is true cout << "Condition met, thread proceeds." << endl; } int main() { thread t(printMessage); this_thread::sleep_for(chrono::seconds(1)); // Simulate work in main { lock_guard<mutex> lock(mtx); ready = true; } cv.notify_one(); // Notify the waiting thread t.join(); return 0; }
Explanation:
- cv.wait(lock, [] { return ready; }); makes the thread wait until ready is true.
- cv.notify_one(); wakes up one waiting thread.
Output:
Condition met, thread proceeds.
7. Using std::future and std::async for Asynchronous Tasks
std::async creates an asynchronous task that returns a std::future to retrieve the result.
#include <iostream> #include <future> using namespace std; int calculateSquare(int x) { return x * x; } int main() { future<int> result = async(calculateSquare, 5); // Launch async task cout << "Square of 5: " << result.get() << endl; // Retrieve the result return 0; }
Explanation:
- async(calculateSquare, 5); runs calculateSquare(5) asynchronously.
- result.get(); waits for the result and retrieves it.
Output:
Square of 5: 25
8. Shared Data with std::atomic
std::atomic provides a way to safely share data without using mutexes.
#include <iostream> #include <thread> #include <atomic> using namespace std; atomic<int> counter(0); void incrementCounter() { for (int i = 0; i < 1000; ++i) { counter++; } } int main() { thread t1(incrementCounter); thread t2(incrementCounter); t1.join(); t2.join(); cout << "Final counter value: " << counter << endl; return 0; }
Explanation:
- counter++ is thread-safe without needing a mutex because counter is atomic.
Output:
Final counter value: 2000
9. Parallel Processing with Multiple Threads
Create multiple threads to perform a task in parallel.
#include <iostream> #include <thread> #include <vector> using namespace std; void task(int id) { cout << "Thread " << id << " is executing." << endl; } int main() { vector<thread> threads; for (int i = 1; i <= 5; ++i) { threads.push_back(thread(task, i)); // Create and add threads to the vector } for (auto& t : threads) { t.join(); // Wait for all threads to finish } return 0; }
Explanation:
- Creates 5 threads, each executing task with a unique id.
- t.join() waits for each thread in the vector to finish.
Output (order may vary):
Thread 1 is executing. Thread 2 is executing. Thread 3 is executing. Thread 4 is executing. Thread 5 is executing.
10. Thread Pool Example
A thread pool manages multiple threads that can execute tasks in parallel.
#include <iostream> #include <vector> #include <thread> #include <queue> #include <mutex> #include <condition_variable> using namespace std; class ThreadPool { vector<thread> workers; queue<function<void()>> tasks; mutex mtx; condition_variable cv; bool stop; public: ThreadPool(size_t threads) : stop(false) { for (size_t i = 0; i < threads; ++i) { workers.emplace_back([this] { while (true) { function<void()> task; { unique_lock<mutex> lock(this->mtx); this->cv.wait(lock, [this] { return this->stop || !this->tasks.empty(); }); if (this->stop && this->tasks.empty()) return; task = move(this->tasks.front()); this->tasks.pop(); } task(); } }); } } template <class F> void enqueue(F&& f) { { unique_lock<mutex> lock(mtx); tasks.emplace(f); } cv.notify_one(); } ~ThreadPool() { { unique_lock<mutex> lock(mtx); stop = true; } cv.notify_all(); for (thread &worker : workers) worker.join(); } }; void task(int id) { cout << "Task " << id << " is executing." << endl; } int main() { ThreadPool pool(3); // Create a thread pool with 3 threads for (int i = 1; i <= 5; ++i) { pool.enqueue([i] { task(i); }); } this_thread::sleep_for(chrono::seconds(1)); // Wait for tasks to finish return 0; }
Explanation:
- The thread pool manages 3 threads, each able to pick up tasks from a queue.
- pool.enqueue([i] { task(i); }); adds tasks to the queue, which the threads then execute.
Output (order may vary):
Task 1 is executing. Task 2 is executing. Task 3 is executing. Task 4 is executing. Task 5 is executing.
Summary Table of Multithreading Examples
Example | Description |
---|---|
Basic Thread Creation | Creates and joins a thread |
Passing Arguments | Passes arguments to a thread function |
Lambda Functions in Threads | Uses a lambda expression in a thread |
Detached Threads | Creates a detached thread |
Mutex for Synchronization | Uses mutex to avoid data races |
Condition Variables | Synchronizes threads with condition variables |
Asynchronous Tasks with std::async | Uses std::async and std::future |
Atomic Variables | Thread-safe counter with std::atomic |
Parallel Processing with Multiple Threads | Runs tasks in parallel with multiple threads |
Thread Pool | Manages a pool of threads for task execution |
Key Takeaways
- Multithreading in C++ is managed with <thread>, allowing concurrent execution of tasks.
- Mutexes and condition variables help synchronize shared resources and coordinate between threads.
- Atomic variables perform thread-safe operations without locking mechanisms.
- Thread pools offer an efficient way to manage a fixed number of threads that process multiple tasks.
- Use std::async and lambda expressions to simplify asynchronous tasks and reduce function boilerplate.