skip to content
Mehdi Mehdikhani
Table of Contents

std::thread in C++11 finally brought standard threading to C++. No more platform-specific pthread or Win32 API - just portable, easy-to-use threading.

What std::thread Actually Does

std::thread wraps a native thread and lets you run functions concurrently:

#include <thread>
#include <iostream>
void worker_function() {
std::cout << "Running in thread: " << std::this_thread::get_id() << std::endl;
}
std::thread t(worker_function);
t.join(); // Wait for thread to complete

Each std::thread object represents one thread of execution.

Creating Threads

You can pass almost anything to a thread constructor:

// Function pointer
void print_message(const std::string& msg) {
std::cout << msg << std::endl;
}
std::thread t1(print_message, "Hello from thread!");
// Lambda
std::thread t2([]() {
std::cout << "Lambda thread!" << std::endl;
});
// Member function
class Worker {
public:
void do_work(int n) {
std::cout << "Working with: " << n << std::endl;
}
};
Worker w;
std::thread t3(&Worker::do_work, &w, 42);
// All threads need to be joined or detached
t1.join();
t2.join();
t3.join();

When I Actually Use std::thread

Most of the time, I use threads for:

  1. Background tasks: Long-running operations that shouldn’t block the main thread:
class FileProcessor {
std::thread background_thread_;
std::atomic<bool> should_stop_{false};
public:
void start_processing() {
background_thread_ = std::thread([this]() {
while (!should_stop_) {
process_pending_files();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
}
void stop() {
should_stop_ = true;
if (background_thread_.joinable()) {
background_thread_.join();
}
}
};
  1. Producer-consumer scenarios: Processing data pipelines:
#include <queue>
#include <mutex>
#include <condition_variable>
class TaskQueue {
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
bool shutdown_ = false;
public:
void add_task(std::function<void()> task) {
{
std::lock_guard<std::mutex> lock(mutex_);
tasks_.push(std::move(task));
}
cv_.notify_one();
}
void worker_thread() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return !tasks_.empty() || shutdown_; });
if (shutdown_ && tasks_.empty()) break;
task = std::move(tasks_.front());
tasks_.pop();
}
task(); // Execute outside of lock
}
}
};
  1. Parallel processing: Breaking work into chunks:
template<typename Iterator, typename Function>
void parallel_for_each(Iterator first, Iterator last, Function func) {
const size_t num_threads = std::thread::hardware_concurrency();
const size_t chunk_size = std::distance(first, last) / num_threads;
std::vector<std::thread> threads;
for (size_t i = 0; i < num_threads; ++i) {
auto chunk_start = first + i * chunk_size;
auto chunk_end = (i == num_threads - 1) ? last : chunk_start + chunk_size;
threads.emplace_back([chunk_start, chunk_end, func]() {
std::for_each(chunk_start, chunk_end, func);
});
}
for (auto& t : threads) {
t.join();
}
}
// Usage
std::vector<int> numbers(10000);
parallel_for_each(numbers.begin(), numbers.end(), [](int& n) {
n = expensive_computation(n);
});
  1. Timeouts and periodic tasks:
class PeriodicTimer {
std::thread timer_thread_;
std::atomic<bool> running_{true};
public:
template<typename Function>
PeriodicTimer(std::chrono::milliseconds interval, Function func) {
timer_thread_ = std::thread([this, interval, func]() {
while (running_) {
auto start = std::chrono::steady_clock::now();
func();
auto elapsed = std::chrono::steady_clock::now() - start;
if (elapsed < interval) {
std::this_thread::sleep_for(interval - elapsed);
}
}
});
}
~PeriodicTimer() {
running_ = false;
if (timer_thread_.joinable()) {
timer_thread_.join();
}
}
};

Thread Lifecycle Management

Threads must be either joined or detached:

void demonstrate_lifecycle() {
std::thread t([]() {
std::cout << "Thread work" << std::endl;
});
// Option 1: Wait for completion
t.join();
// Option 2: Detach and let it run independently
// t.detach(); // Thread becomes daemon-like
// Option 3: Check if joinable first
if (t.joinable()) {
t.join();
}
// NOT doing any of these = std::terminate when t destructor runs!
}

Thread-Safe Communication

Use atomic variables or mutexes for thread communication:

class ThreadSafeCounter {
std::atomic<int> count_{0};
public:
void increment() { ++count_; }
int get() const { return count_; }
};
class DataSharer {
std::vector<int> shared_data_;
mutable std::mutex mutex_;
public:
void add_data(int value) {
std::lock_guard<std::mutex> lock(mutex_);
shared_data_.push_back(value);
}
std::vector<int> get_copy() const {
std::lock_guard<std::mutex> lock(mutex_);
return shared_data_; // Return copy
}
};

Real World Example

Here’s a simple thread pool I built:

class ThreadPool {
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex queue_mutex_;
std::condition_variable condition_;
bool stop_ = false;
public:
ThreadPool(size_t num_threads) {
for (size_t i = 0; i < num_threads; ++i) {
workers_.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
condition_.wait(lock, [this] { return stop_ || !tasks_.empty(); });
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
template<typename F>
void enqueue(F&& f) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
tasks_.emplace(std::forward<F>(f));
}
condition_.notify_one();
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
stop_ = true;
}
condition_.notify_all();
for (std::thread& worker : workers_) {
worker.join();
}
}
};
// Usage
ThreadPool pool(4);
pool.enqueue([]() { std::cout << "Task 1" << std::endl; });
pool.enqueue([]() { std::cout << "Task 2" << std::endl; });

Common Gotchas

  1. Always join or detach - forgetting this calls std::terminate
  2. Data races - always protect shared data with synchronization
  3. Exception safety - exceptions in threads can terminate the program
  4. Resource cleanup - make sure threads clean up properly

The Pattern I Follow

For most use cases, I prefer higher-level abstractions like std::async or thread pools over raw std::thread:

// Raw thread - more manual work
std::thread t(compute_something, data);
t.join();
// std::async - often easier
auto future = std::async(std::launch::async, compute_something, data);
auto result = future.get();

But std::thread is essential when you need precise control over thread lifetime and behavior. It’s the foundation that everything else builds on.