Standard Thread Initialization
C++11 introduced std::thread as a portable, standardized alternative to platform-specific threading APIs like POSIX pthreads. Unlike legacy approaches requiring manual linking with -lpthread, modern C++ threads rely solely on the standard library and proper compiler flags (-std=c++17 -pthread).
The following example launches five concurrent tasks using std::thread, each printing a unique identifier:
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
void worker_task(int id) {
std::cout << "Worker " << id << " started\n";
// Simulate work
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << "Worker " << id << " completed\n";
}
int main() {
const int num_workers = 5;
std::vector<std::thread> workers;
for (int i = 0; i < num_workers; ++i) {
workers.emplace_back(worker_task, i);
}
for (auto& t : workers) {
if (t.joinable()) {
t.join();
}
}
return 0;
}
This implementation avoids non-portable headers like <pthread.h> and eliminates the need for explicit library linking on most systems.
File I/O Benchmarking with Concurrency
To empirically evaluate concurrency benefits, consider reading multiple large files in parallel versus sequentially. The following benchmark compares both strategies using std::thread and measures wall-clock time:
#include <iostream>
#include <fstream>
#include <thread>
#include <vector>
#include <chrono>
#include <filesystem>
namespace fs = std::filesystem;
void read_file(const std::string& path) {
std::ifstream stream(path, std::ios::binary);
stream.seekg(0, std::ios::end);
auto size = stream.tellg();
stream.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
stream.read(buffer.data(), size);
}
int main() {
std::vector<std::string> file_paths = {
"data/part00.txt",
"data/part01.txt",
"data/CMakeLists.txt"
};
// Concurrent read
auto start = std::chrono::steady_clock::now();
std::vector<std::thread> readers;
for (const auto& p : file_paths) {
readers.emplace_back(read_file, p);
}
for (auto& t : readers) {
if (t.joinable()) t.join();
}
auto concurrent_end = std::chrono::steady_clock::now();
// Sequential read
for (const auto& p : file_paths) {
read_file(p);
}
auto sequential_end = std::chrono::steady_clock::now();
auto concurrent_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
concurrent_end - start).count();
auto sequential_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
sequential_end - concurrent_end).count();
std::cout << "Concurrent: " << concurrent_ms << " ms\n";
std::cout << "Sequential: " << sequential_ms << " ms\n";
return 0;
}
On systems with sufficient I/O bandwidth and CPU cores, concurrent execution typically yields measurable improvements—especially when handling many medium-to-large files. Observed speedups scale with hardware concurrency and I/O parallelism, not just CPU utilization.
Race Conditions and Mutual Exclusion
Shared mutable state across threads requires synchronization. Consider this unsafe counter increment:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int shared_counter = 0;
std::mutex counter_mutex;
void unsafe_increment() {
for (int i = 0; i < 1000; ++i) {
++shared_counter; // Race condition here
}
}
void safe_increment() {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(counter_mutex);
++shared_counter;
}
}
int main() {
const int thread_count = 8;
std::vector<std::thread> threads;
// Unsafe version — produces inconsistent results
for (int i = 0; i < thread_count; ++i) {
threads.emplace_back(unsafe_increment);
}
for (auto& t : threads) t.join();
std::cout << "Unsafe result: " << shared_counter << '\n';
shared_counter = 0;
threads.clear();
// Safe version — guarantees correctness
for (int i = 0; i < thread_count; ++i) {
threads.emplace_back(safe_increment);
}
for (auto& t : threads) t.join();
std::cout << "Safe result: " << shared_counter << '\n';
return 0;
}
Without std::mutex, interleaved memory operations corrupt the counter. std::lock_guard ensures automtaic lock release, preventing deadlocks from early returns or exceptions.
Minimal Thread Pool Implementation
A reusable thread pool decouples task submission from execution infrastructure. Below is a simplified, exception-safe implementation using std::queue, std::condition_variable, and RAII-compliant lifetimes:
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <vector>
#include <atomic>
class SimpleThreadPool {
public:
explicit SimpleThreadPool(size_t threads = std::thread::hardware_concurrency())
: stop_requested_(false) {
for (size_t i = 0; i < threads; ++i) {
workers_.emplace_back(&SimpleThreadPool::worker_loop, this);
}
}
~SimpleThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex_);
stop_requested_ = true;
}
condition_.notify_all();
for (auto& t : workers_) {
if (t.joinable()) t.join();
}
}
template<typename F, typename... Args>
void enqueue(F&& f, Args&&... args) {
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks_.emplace([=]() { std::forward<F>(f)(std::forward<Args>(args)...); });
}
condition_.notify_one();
}
private:
void worker_loop() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
condition_.wait(lock, [this] { return stop_requested_ || !tasks_.empty(); });
if (stop_requested_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
}
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex queue_mutex_;
std::condition_variable condition_;
std::atomic<bool> stop_requested_;
};
// Usage example
#include <iostream>
int main() {
SimpleThreadPool pool(4);
for (int i = 0; i < 12; ++i) {
pool.enqueue([](int id) {
std::cout << "Executing task " << id << " on thread "
<< std::this_thread::get_id() << '\n';
}, i);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 0;
}
This design uses std::atomic<bool> for the shutdown flag, avoids raw pointers, leverages move semantics for task storage, and enforces deterministic cleanup via RAII.
Static Member Access in Multithreaded Contexts
Static members are shared across all instances of a class—and across all threads. Direct access without synchronization leads to data races. For example:
class Counter {
public:
static int value;
static void increment() { ++value; } // Unsafe!
};
int Counter::value = 0;
Even if increment() is invoked through distinct object instances, it modifies the same global variable. Thread safety must be enforced externally or encapsulated within the class using std::atomic<int> or mutex protection. Prefer std::atomic for simple scalar updates due to lower overhead and guaranteed memory ordering.