Mastering Concurrency with C++11 Threads
Thread Instantiation and Management
The C++11 <thread> library provides a standardized interface for managing concurrent execution. A thread is initiated by passing a callable entity—such as a function pointer, functor, or lambda expression—to the std::thread constructor, along with its required arguments.
#include <iostream>
#include <thread>
void executeTask(const std::string& message) {
std::cout << "Thread executing: " << message << std::endl;
}
int main() {
// Create a thread using a function pointer and arguments
std::thread worker(executeTask, "Processing Data");
// Detach the thread to run independently
// Note: Ensure the thread does not access stack variables from main after main exits
worker.detach();
// Check joinability before attempting to join
std::thread anotherWorker(executeTask, "Background Task");
if (anotherWorker.joinable()) {
anotherWorker.join(); // Blocks until the thread finishes
}
return 0;
}
Handling Argument Passing and Lifetimes
Passing arguments to threads requires careful consideration of object lifetimes and copy semantics. By default, arguments are copied into the thread's storage. To pass a reference, std::ref must be used. Furthermore, ensuring that pointed-to objects outlive the thread is critical to avoid undefined behavior.
#include <iostream>
#include <thread>
#include <memory>
void incrementCounter(int& counter) {
counter += 10;
}
// Potential error: Passing a reference to a temporary or local variable that dies too soon
// Correct approach: Use std::ref or ensure object longevity
struct DataProcessor {
void process() {
std::cout << "Processing data..." << std::endl;
}
};
int main() {
int value = 5;
std::thread t1(incrementCounter, std::ref(value));
t1.join();
std::cout << "Value after thread: " << value << std::endl;
// Using smart pointers to manage object lifetime in threads
auto processorPtr = std::make_shared<DataProcessor>();
std::thread t2(&DataProcessor::process, processorPtr);
t2.join();
return 0;
}
Mutexes and Data Race Prevention
A data race occurs when multiple threads access shared memory concurrently, and at least one performs a write. std::mutex provides an exclusive lock mechanism to synchronize access to critical sections.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
int globalCounter = 0;
std::mutex counterMutex;
void safeIncrement() {
for (int i = 0; i < 1000; ++i) {
// Lock the mutex before modifying the shared resource
std::lock_guard<std::mutex> lock(counterMutex);
globalCounter++;
// Mutex automatically unlocks when 'lock' goes out of scope
}
}
int main() {
std::thread t1(safeIncrement);
std::thread t2(safeIncrement);
t1.join();
t2.join();
std::cout << "Final Counter Value: " << globalCounter << std::endl;
return 0;
}
Deadlock Avoidance and Lock Strategies
Deadlock arises when two or more threads wait indefinitely for each other to release locks. This typically happens when locks are acquired in inconsistent orders. Standardizing the lock sequence across all threads resolves this. Additionally, std::unique_lock offers greater flexibility than std::lock_guard, supporting manual locking, try-lock mechanisms, and deferred locking.
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex resourceA;
std::mutex resourceB;
void workerOne() {
// Consistent locking order: A then B
std::lock_guard<std::mutex> lockA(resourceA);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lockB(resourceB);
std::cout << "Worker 1 acquired both resources" << std::endl;
}
void workerTwo() {
// Consistent locking order: A then B (prevents deadlock)
std::unique_lock<std::mutex> lockA(resourceA);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::unique_lock<std::mutex> lockB(resourceB);
std::cout << "Worker 2 acquired both resources" << std::endl;
}
// Example of try_lock_for with timed_mutex
#include <timed_mutex>
std::timed_mutex timedMtx;
void attemptTimedLock() {
std::unique_lock<std::timed_mutex> lock(timedMtx, std::defer_lock);
if (lock.try_lock_for(std::chrono::seconds(1))) {
std::cout << "Lock acquired successfully" << std::endl;
} else {
std::cout << "Failed to acquire lock within timeout" << std::endl;
}
}
Thread-Safe Initialization with Call Once
The std::call_once mechanism ensures that a specific function is invoked exactly once, even in the presence of multiple concurrent threads. This is particularly useful for implementing thread-safe lazy initialization in the Singleton pattern.
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag initFlag;
class DatabaseConnection {
private:
DatabaseConnection() { std::cout << "Database connection established." << std::endl; }
public:
static DatabaseConnection& getInstance() {
std::call_once(initFlag, []() {
instance = new DatabaseConnection();
});
return *instance;
}
void query(const std::string& sql) {
std::cout << "Executing: " << sql << std::endl;
}
private:
static DatabaseConnection* instance;
};
DatabaseConnection* DatabaseConnection::instance = nullptr;
void performRequest(int id) {
DatabaseConnection::getInstance().query("SELECT * FROM users WHERE id = " + std::to_string(id));
}
Condition Variables and Producer-Consumer Logic
std::condition_variable allows threads to wait until a specific condition becomes true. It is commonly used to implement producer-consumer workflows where one thread generates data and another processes it. The consumer waits on the condition variable when the queue is empty, and the producer notifies the consumer when new data arrives.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> taskQueue;
std::mutex queueMutex;
std::condition_variable queueCondVar;
void producer(int taskId) {
{
std::lock_guard<std::mutex> lock(queueMutex);
taskQueue.push(taskId);
std::cout << "Produced task: " << taskId << std::endl;
}
// Notify one waiting thread that data is available
queueCondVar.notify_one();
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(queueMutex);
// Wait until the queue is not empty
queueCondVar.wait(lock, []() { return !taskQueue.empty(); });
int taskId = taskQueue.front();
taskQueue.pop();
lock.unlock(); // Unlock before processing to allow other producers
std::cout << "Consumed task: " << taskId << std::endl;
if (taskId == -1) break; // Exit signal
}
}