Multithreading and Concurrency in C++
Multithreading and concurrency are essential concepts in modern programming that allow your programs to perform multiple tasks simultaneously. This chapter will guide you through multithreading in C++, from the basics to advanced concepts, including thread management, synchronization, and best practices.
What is Multithreading?
- Multithreading is a programming technique where multiple threads run in parallel within a single process.
- A thread is the smallest unit of execution.
- C++ provides extensive support for multithreading through the <thread> library.
Why Use Multithreading?
- Increases application performance (parallel execution).
- Utilizes multi-core processors efficiently.
- Handles multiple tasks at the same time (e.g., I/O operations, computations).
Basics of Multithreading in C++
- C++ provides a standard library for multithreading (<thread>, <mutex>, <condition_variable>).
- Each thread runs independently but shares the same memory space with other threads of the same process.
Creating Threads
- Threads are created using the std::thread class.
- A thread can be created by:
- Using a function.
- Using a lambda function.
- Using a member function of a class.
➡️ Example: Creating a Simple Thread
#include <iostream>
#include <thread>
using namespace std;
// Function to be executed by the thread
void printMessage() {
cout << "Hello from thread!" << endl;
}
int main() {
// Creating a thread that executes the printMessage function
thread t1(printMessage);
// Ensuring the main thread waits for t1 to complete
t1.join();
cout << "Hello from main thread!" << endl;
return 0;
}
Output:
Hello from thread!
Hello from main thread!
Explanation:
- The thread t1 is created and executes the printMessage function.
- The t1.join() ensures that the main thread waits for t1 to complete.
- Without join(), the main thread may finish before t1.
Thread Management in C++
1. Joining Threads (.join())
- Ensures that a thread completes before the main thread continues.
- Must be called for each created thread.
2. Detaching Threads (.detach())
- Separates the thread from the main thread, allowing it to run independently.
➡️ Example: Detaching a Thread
#include <iostream>
#include <thread>
using namespace std;
void printMessage() {
cout << "Detached Thread Running..." << endl;
}
int main() {
thread t1(printMessage);
t1.detach(); // Detaches the thread
cout << "Main Thread Exiting..." << endl;
return 0;
}
Output (Order may vary):
Main Thread Exiting...
Detached Thread Running...
Explanation:
- The detached thread runs independently.
- The main thread does not wait for the detached thread.
3. Checking Thread Joinability (.joinable())
- Ensures that a thread can be joined.
- Always check before joining a thread.
➡️ Example:
#include <iostream>
#include <thread>
using namespace std;
void task() {
cout << "Task executed in thread." << endl;
}
int main() {
thread t(task);
if (t.joinable()) {
t.join();
}
return 0;
}
Best Practices:
- Always join or detach a thread before program termination.
- Avoid creating too many threads (can lead to overhead).
Passing Arguments to Threads
- Arguments can be passed to threads using value, reference, or pointer.
➡️ Example: Passing Arguments by Value
#include <iostream>
#include <thread>
using namespace std;
void printSum(int a, int b) {
cout << "Sum: " << a + b << endl;
}
int main() {
thread t(printSum, 5, 3);
t.join();
return 0;
}
Output:
Sum: 8
➡️ Example: Passing Arguments by Reference
#include <iostream>
#include <thread>
using namespace std;
void increment(int &num) {
num++;
}
int main() {
int value = 10;
thread t(increment, ref(value)); // Use ref() for reference
t.join();
cout << "Value after increment: " << value << endl;
return 0;
}
Output:
Value after increment: 11
Thread Synchronization
- When multiple threads access shared resources, data races can occur.
- Synchronization ensures that only one thread accesses a resource at a time.
Mutex (Mutual Exclusion)
- Prevents multiple threads from accessing the same resource simultaneously.
➡️ Example: Using Mutex
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx; // Mutex object
void printSafeMessage() {
mtx.lock();
cout << "Thread-Safe Message" << endl;
mtx.unlock();
}
int main() {
thread t1(printSafeMessage);
thread t2(printSafeMessage);
t1.join();
t2.join();
return 0;
}
Output:
Thread-Safe Message
Thread-Safe Message
✅Best Practices for Mutex:
- Use lock_guard or scoped_lock for automatic unlocking.
- Avoid manually unlocking a mutex (can cause deadlock).
Deadlock and How to Avoid It
- Deadlock occurs when two or more threads are waiting for each other to release a lock.
- Common Causes:
- Locking multiple mutexes in different orders.
- Forgetting to unlock a mutex.
➡️ Example: Deadlock (Incorrect)
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx1, mtx2;
void threadA() {
lock_guard<mutex> lock1(mtx1);
this_thread::sleep_for(chrono::milliseconds(100)); // Simulate delay
lock_guard<mutex> lock2(mtx2);
}
void threadB() {
lock_guard<mutex> lock2(mtx2);
this_thread::sleep_for(chrono::milliseconds(100)); // Simulate delay
lock_guard<mutex> lock1(mtx1);
}
int main() {
thread t1(threadA);
thread t2(threadB);
t1.join();
t2.join();
return 0;
}
✅ Solution: Avoiding Deadlock with std::lock()
void threadA() {
lock(mtx1, mtx2);
lock_guard<mutex> lock1(mtx1, adopt_lock);
lock_guard<mutex> lock2(mtx2, adopt_lock);
}
Advanced Thread Synchronization
1. Condition Variables
- Allow one thread to wait for a condition to be met.
2. Atomic Variables
- Provide lock-free thread safety for basic data types.
Best Practices for Multithreading
- Avoid creating too many threads (thread pool is better).
- Use std::lock_guard or std::scoped_lock instead of manual locks.
- Always check if a thread is joinable.
- Avoid data races using mutexes or atomic variables.
- Minimize the use of global variables.
Summary
- Multithreading in C++ allows parallel execution of tasks.
- Threads can be managed using .join() and .detach().
- Synchronization is essential for safe multithreading (mutex, lock_guard).
- Avoid deadlock using std::lock() or consistent locking order.