Introduction
Multithreading in C++ allows a program to execute multiple threads concurrently, enhancing performance, responsiveness, and efficient CPU utilization. A thread is the smallest sequence of programmed instructions that can be managed independently by a scheduler. Starting with C++11, the <thread> library provides robust support for thread creation, management, and synchronization. By splitting tasks into threads, C++ programs can perform multiple operations in parallel—crucial for modern computing where responsiveness and concurrency are vital. This section explores thread creation, joining, detaching, mutexes, deadlocks, thread safety, and condition variables with proper syntax, examples, and deeper insight.
std::thread and Thread Creation
The std::thread class is introduced in C++11 and allows developers to create and manage threads. Threads can be launched by passing a function (or callable object) to the std::thread constructor. Once a thread is launched, it runs concurrently with the main thread.
General Syntax:
#include <iostream>
#include <thread>
void functionName() {
// Code to run in thread
}
int main() {
std::thread t1(functionName); // Start thread
t1.join(); // Wait for thread to finish
return 0;
}
Example:
#include <iostream>
#include <thread>
using namespace std;
void printNumbers() {
for (int i = 1; i <= 5; i++)
cout << “Number: ” << i << endl;
}
int main() {
thread t1(printNumbers); // Create a new thread
t1.join(); // Main thread waits for t1 to finish
return 0;
}
Joining & Detaching Threads
The join() function blocks the current thread until the thread it’s called on finishes execution. In contrast, detach() allows the thread to run independently (in the background), and its resources are automatically released upon completion.
Syntax:
thread t1(functionName);
t1.join(); // Waits for thread
// OR
t1.detach(); // Detaches thread (runs independently)
Example:
void backgroundTask() {
cout << “Running in background…” << endl;
}
int main() {
thread t(backgroundTask);
t.detach(); // Now it runs independently
cout << “Main thread ends.” << endl;
return 0;
}
Do not call both join() and detach() on the same thread. It causes a runtime error.
Mutex, Lock, and Deadlock
A mutex (mutual exclusion) is used to protect shared data from being accessed simultaneously by multiple threads. If not managed properly, mutexes can cause deadlocks where two or more threads wait indefinitely for each other to release resources.
Syntax:
#include <mutex>
mutex m;
void safeAccess() {
m.lock(); // Lock the critical section
// Access shared data
m.unlock(); // Unlock it
}
Example (with lock_guard):
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex m;
void printSafe(string msg) {
lock_guard<mutex> guard(m); // Automatically locks and unlocks
cout << msg << endl;
}
int main() {
thread t1(printSafe, “Hello”);
thread t2(printSafe, “World”);
t1.join();
t2.join();
return 0;
}
Use lock_guard or unique_lock for exception-safe locking.
Deadlock Example & Avoidance
Deadlocks occur when multiple threads are blocked forever, waiting for each other. This usually happens when locks are acquired in an inconsistent order across threads.
Deadlock Example:
mutex m1, m2;
void task1() {
lock_guard<mutex> lock1(m1);
this_thread::sleep_for(chrono::milliseconds(100));
lock_guard<mutex> lock2(m2); // waits for m2
}
void task2() {
lock_guard<mutex> lock2(m2);
lock_guard<mutex> lock1(m1); // waits for m1
}
Avoidance: Use std::lock() to lock multiple mutexes atomically:
std::lock(m1, m2);
lock_guard<mutex> lock1(m1, adopt_lock);
lock_guard<mutex> lock2(m2, adopt_lock);
Thread-safe Practices
Thread safety means that shared data structures are protected and accessed in a way that prevents race conditions. This often involves the use of synchronization primitives like mutexes, atomic variables, or thread-safe containers.
Tips:
- Avoid sharing state unnecessarily.
- Use std::atomic for simple atomic operations.
- Always lock shared resources.
- Prefer lock_guard or unique_lock over manual locking.
Example with atomic:
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; ++i)
++counter;
}
int main() {
thread t1(increment), t2(increment);
t1.join(); t2.join();
cout << “Counter = ” << counter << endl;
}
Condition Variables
Condition variables allow threads to wait (block) until a particular condition is true. They are useful for thread communication and coordination (like producer-consumer problems).
Syntax:
#include <condition_variable>
condition_variable cv;
mutex m;
bool ready = false;
void waitTask() {
unique_lock<mutex> lk(m);
cv.wait(lk, []{ return ready; }); // Wait until ready == true
// Continue execution
}
Example:
#include <iostream>
#include <thread>
#include <condition_variable>
using namespace std;
mutex m;
condition_variable cv;
bool ready = false;
void waitForSignal() {
unique_lock<mutex> lock(m);
cv.wait(lock, []{ return ready; });
cout << “Thread resumed after signal!” << endl;
}
void sendSignal() {
this_thread::sleep_for(chrono::seconds(1));
{
lock_guard<mutex> lock(m);
ready = true;
}
cv.notify_one(); // Send signal
}
int main() {
thread t1(waitForSignal);
thread t2(sendSignal);
t1.join();
t2.join();
return 0;
}
Advantages of Multithreading in C++
- Improved Performance: Multiple threads can run concurrently and utilize multi-core processors effectively.
- Responsiveness: Applications stay responsive (e.g., UI doesn’t freeze) while doing background tasks.
- Resource Sharing: Threads share the same memory space, making data exchange faster and easier.
- Efficient Parallelism: Allows performing independent tasks like downloading and processing simultaneously.
Disadvantages of Multithreading
- Complex Debugging: Multithreaded code can be difficult to debug due to non-deterministic behavior.
- Race Conditions: Improper handling of shared resources may lead to race conditions or corrupted data.
- Deadlocks: Threads waiting for each other’s resources can cause deadlocks, halting program execution.
- Overhead: Creating and managing threads adds overhead and complexity.
Applications of Multithreading
- Gaming and Graphics: For rendering, physics, and event handling in parallel.
- Web Servers: To handle multiple client requests simultaneously.
- Scientific Computing: Performing parallel matrix operations or simulations.
- Real-Time Systems: Embedded systems that perform real-time monitoring or control.
Limitations of Multithreading
- Thread Creation Limit: Systems can only create a finite number of threads depending on memory and OS.
- Context Switching Cost: Frequent context switching between threads can degrade performance.
- Shared Memory Risks: Mismanagement of shared memory can lead to hard-to-find bugs.
- Platform Dependency: Threading behavior and limits may vary across platforms and compilers.