Skip to content

Concurrency and Threads — std::thread, std::async, std::future, std::promise, Thread Pools

DodaTech Updated 2026-06-28 8 min read

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

  1. 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.

  2. What happens if a std::thread destructor runs while it is still joinable? Answer: std::terminate is called. Always join or detach before destruction.

  3. 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.

  4. 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.

  5. How do you fix a data race? Answer: Use std::mutex (mutual exclusion) or std::atomic (lock-free operations) to protect shared data.

FAQ

What threading primitives does C++ provide

std::thread, std::async, std::future, std::promise, std::packaged_task, std::mutex, std::condition_variable, std::atomic, and std::jthread (C++20).

Is std::thread a wrapper for OS threads

Yes. std::thread directly creates an OS thread (pthreads on Linux, Win32 threads on Windows) with no additional abstraction overhead.

What is the difference between future and shared_future

future is move-only and get() can be called once. shared_future is copyable and get() can be called by multiple consumers.

Does std::async guarantee thread creation

Only with std::launch::async policy. The default policy lets the implementation choose between async and deferred.

What is std::jthread (C++20)

std::jthread is a joining thread that automatically joins in its destructor and supports cooperative cancellation via std::stop_token.

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