Concurrency and Threads — std::thread, std::async, std::future, std::promise, Thread Pools
In this tutorial, you will learn about Concurrency and Threads. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ concurrency uses std::thread for OS threads, std::async for fire-and-forget tasks, and std::future/promise for asynchronous value transfer — all with RAII-based thread management.
What You'll Learn
You will create and manage threads with std::thread, use std::async for asynchronous tasks, transfer values between threads with std::future and std::promise, implement thread pools for efficient task execution, handle thread synchronization (mutex, condition_variable), and avoid common concurrency pitfalls like data races and deadlocks.
Why It Matters
Modern CPUs have multiple cores — single-threaded utilization leaves performance on the table. C++ provides portable, zero-overhead threading primitives that map directly to OS threads. Understanding concurrency is essential for game engines, web servers, data processing, and any application that needs to utilize modern hardware fully.
Learning Path
graph LR
A["61: Design Patterns in C++"] --> B["62: Concurrency & Threads"]
B --> C["63: Atomics & Synchronization"]
C --> D["64: File I/O & Serialization"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
std::thread Basics
std::thread creates an OS thread that executes a callable.
#include <iostream>
#include <thread>
#include <chrono>
void worker(int id, const std::string& task) {
std::cout << "Thread " << id << " starting: " << task << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Thread " << id << " finished\n";
}
int main() {
// Start two threads
std::thread t1(worker, 1, "Download file");
std::thread t2(worker, 2, "Process data");
std::cout << "Main thread continuing...\n";
// Wait for threads to finish (RAII: thread must be joined or detached before destruction)
t1.join();
t2.join();
std::cout << "All threads done\n";
}
Thread RAII — Joining in Destructor
Always join or detach a thread before its destructor runs. A wrapper simplifies this.
#include <iostream>
#include <thread>
#include <chrono>
class ThreadRAII {
std::thread thread_;
public:
explicit ThreadRAII(std::thread t) : thread_(std::move(t)) {
if (!thread_.joinable()) {
throw std::logic_error("Thread not joinable");
}
}
~ThreadRAII() {
if (thread_.joinable()) {
thread_.join(); // Or detach — choose your policy
}
}
ThreadRAII(const ThreadRAII&) = delete;
ThreadRAII& operator=(const ThreadRAII&) = delete;
std::thread& get() { return thread_; }
};
int main() {
{
ThreadRAII t(std::thread([]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "RAII thread done\n";
}));
// Automatically joined at end of scope
}
std::cout << "Thread was joined automatically\n";
}
std::async and std::future
std::async runs a function asynchronously and returns a std::future to retrieve the result.
#include <iostream>
#include <future>
#include <chrono>
#include <vector>
#include <numeric>
int computeHeavy(int n) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return n * n;
}
int main() {
// Launch async tasks
std::future<int> f1 = std::async(std::launch::async, computeHeavy, 5);
std::future<int> f2 = std::async(std::launch::async, computeHeavy, 10);
// Do other work while they compute...
std::cout << "Main thread working while async tasks run\n";
// Get results (blocks if not ready)
int result1 = f1.get(); // Blocks until ready
int result2 = f2.get();
std::cout << "Results: " << result1 << ", " << result2 << "\n"; // 25, 100
// Launch policies:
// std::launch::async — run on new thread
// std::launch::deferred — run on first get()/wait()
// default — implementation chooses
// std::future with exceptions
auto badTask = std::async(std::launch::async, []() {
throw std::runtime_error("async error");
return 42;
});
try {
badTask.get(); // Exception is rethrown here
} catch (const std::exception& e) {
std::cout << "Caught: " << e.what() << "\n";
}
}
std::promise and std::future
std::promise manually sets a value that can be retrieved through its associated std::future.
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
void producer(std::promise<int> promise) {
std::this_thread::sleep_for(std::chrono::milliseconds(300));
int result = 42;
promise.set_value(result); // Fulfill the promise
}
void consumer(std::future<int> future) {
std::cout << "Waiting for result...\n";
int value = future.get(); // Blocks until set_value
std::cout << "Got: " << value << "\n";
}
int main() {
std::promise<int> promise;
std::future<int> future = promise.get_future();
std::thread prod(producer, std::move(promise));
std::thread cons(consumer, std::move(future));
prod.join();
cons.join();
}
Thread Pools
Creating threads per task is expensive. A thread pool reuses threads.
#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <future>
class ThreadPool {
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
bool stop_ = false;
public:
explicit ThreadPool(size_t numThreads) {
for (size_t i = 0; i < numThreads; ++i) {
workers_.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock lock(mutex_);
cv_.wait(lock, [this]() {
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
~ThreadPool() {
{
std::lock_guard lock(mutex_);
stop_ = true;
}
cv_.notify_all();
for (auto& worker : workers_) {
worker.join();
}
}
template <typename F, typename... Args>
auto enqueue(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
using ReturnType = decltype(f(args...));
auto task = std::make_shared<std::packaged_task<ReturnType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<ReturnType> result = task->get_future();
{
std::lock_guard lock(mutex_);
tasks_.emplace([task]() { (*task)(); });
}
cv_.notify_one();
return result;
}
};
int main() {
ThreadPool pool(4);
// Enqueue work
std::vector<std::future<int>> results;
for (int i = 0; i < 10; ++i) {
results.push_back(pool.enqueue([i]() {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return i * i;
}));
}
// Collect results
for (auto& result : results) {
std::cout << result.get() << " ";
}
std::cout << "\n"; // 0 1 4 9 16 25 36 49 64 81
}
std::packaged_task
std::packaged_task wraps a callable for asynchronous execution.
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int add(int a, int b) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return a + b;
}
int main() {
// Wrap a function
std::packaged_task<int(int, int)> task(add);
std::future<int> result = task.get_future();
// Execute on a thread
std::thread t(std::move(task), 3, 4);
// Get result
std::cout << "3 + 4 = " << result.get() << "\n"; // 7
t.join();
// packaged_task with lambda
std::packaged_task<int()> lambdaTask([]() {
return 42;
});
auto future = lambdaTask.get_future();
lambdaTask(); // Execute in current thread
std::cout << "Lambda result: " << future.get() << "\n"; // 42
}
Shared Futures (std::shared_future)
std::future is move-only and get() can only be called once. std::shared_future can be copied and get() called multiple times.
#include <iostream>
#include <future>
#include <thread>
int main() {
std::promise<int> promise;
std::shared_future<int> shared = promise.get_future().share();
// Multiple consumers can read the same result
auto consumer = [shared](int id) {
std::cout << "Consumer " << id << " got: " << shared.get() << "\n";
};
std::thread c1(consumer, 1);
std::thread c2(consumer, 2);
std::thread c3(consumer, 3);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
promise.set_value(99);
c1.join();
c2.join();
c3.join();
}
Common Mistakes
Mistake 1: Not joining or detaching a thread
void bad() {
std::thread t(worker);
// t's destructor calls std::terminate if still joinable!
}
Always join or detach before destruction.
Mistake 2: Data races on shared data
int shared = 0;
std::thread t1([&]() { for (int i = 0; i < 100000; ++i) ++shared; });
std::thread t2([&]() { for (int i = 0; i < 100000; ++i) ++shared; });
Use std::atomic or std::mutex to protect shared data.
Mistake 3: Calling get() twice on the same future
std::future<int> f = std::async([]{ return 42; });
f.get(); // OK
f.get(); // Error: future already retrieved
Use shared_future if multiple reads are needed.
Mistake 4: Deadlock from mutex ordering
std::mutex m1, m2;
// Thread 1: lock m1, then m2
// Thread 2: lock m2, then m1 // Deadlock!
Always lock mutexes in the same order or use std::lock(m1, m2).
Mistake 5: std::async with default policy surprises
auto f = std::async([]() { /* slow */ });
// Default policy could be deferred or async — implementation chooses
// Use std::launch::async to force thread execution
Practice Questions
What is the difference between std::thread and std::async? Answer: std::thread always creates an OS thread. std::async may defer execution or reuse threads, returning a future for the result.
What happens if a std::thread destructor runs while it is still joinable? Answer: std::terminate is called. Always join or detach before destruction.
How does a thread pool improve performance? Answer: It avoids the overhead of creating and destroying threads per task. Threads are reused from a fixed pool.
What is std::promise used for? Answer: To manually set a value (or exception) that can be retrieved through a std::future, enabling thread synchronization.
How do you fix a data race? Answer: Use std::mutex (mutual exclusion) or std::atomic (lock-free operations) to protect shared data.
FAQ
Mini Project
Implement a parallel map function that applies a transformation to each element of a vector using a thread pool:
#include <iostream>
#include <vector>
#include <thread>
#include <algorithm>
#include <future>
#include <functional>
// Your parallel_map function
int main() {
std::vector<int> data(100);
std::iota(data.begin(), data.end(), 0);
// Apply square in parallel
auto result = parallel_map(data, [](int x) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
return x * x;
});
// Verify first 10 elements
for (size_t i = 0; i < 10; ++i) {
std::cout << result[i] << " ";
}
std::cout << "\n"; // 0 1 4 9 16 25 36 49 64 81
std::cout << "Processed " << result.size() << " elements in parallel\n";
}
This project demonstrates how C++ threading primitives build parallel algorithms — similar to std::execution::parallel_policy in C++17 and comparable to parallel streams in Java.
What's Next
You now create and manage threads with C++ concurrency primitives. Next, you will dive deeper into atomics and synchronization — lock-free programming, memory ordering, and safe concurrent data structures.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro