← Back to list

Understanding Threads in C++

Modern applications must do multiple things at once: handle user input, fetch data, render UI, and process background tasks. To achieve…

Seulgie Han · 2026-01-22 09:14 · 3 claps · 3.2 min read
#concurrent-programming #threads
Open on Medium ↗
Wiki topics: GEN · Genomics & Sequencing 💻 · Programming

Understanding Threads in C++

Modern applications must do multiple things at once: handle user input, fetch data, render UI, and process background tasks. To achieve this efficiently, we rely on concurrency, and in C++ that usually means threads. This post walks step-by-step through:

  • Processes vs Threads
  • Creating threads in C++
  • join() vs detach()
  • Passing data to threads safely
  • Understanding std::promise and std::future with a concrete example

Processes vs Threads (Restaurant Analogy)

Think of processes as separate restaurants.

  • Each restaurant has its own kitchen, staff, and inventory
  • One restaurant cannot directly access another restaurant’s kitchen
  • Communication requires phone calls, emails, or delivery trucks (IPC)
  • This is safe, but slow and expensive

Benefit:

If one restaurant burns down, the others keep running. That’s why browsers often use one process per tab.

Now think of threads as kitchen staff inside the same restaurant.

  • They share the same kitchen
  • They share ingredients
  • They share tools and workspace

This makes communication very fast, but introduces new problems.

What happens if two cooks grab the same pan at the same time?

That’s the core trade-off:

  • Processes -> safety through isolation
  • Threads -> performance through sharing

Creating Threads in C++

Basic Thread Creation

#include <thread>
#include <iostream>

void prep_vegetables() {
    std::cout << "Chopping vegetables" << std::endl;
}

int main() {
    std::thread prep_cook(prep_vegetables);
    prep_cook.join();
}

A thread is simply: “Run this function on another execution path”

Calling join() means: “Wait until this worker finishes before continuing”

Lambda-Based Threads

std::thread grill_cook([]() {
    std::cout << "Grilling steaks" << std::endl;
});
grill_cook.join();

Lambdas are useful for short, one-off tasks without defining a separate function.

What Happens Without Proper Management?

The code below has multiple threads but never joined or detached. As a result, output appears mixed and unpredictable and program crashes with terminate called without an active exception. Because C++ requires every thread to be either joined or detached before its destructor runs. This is not optional.

std::thread t1(thread_function, 1);
std::thread t2(thread_function, 2);
std::thread t3(thread_function, 3);
std::thread t4([]() {
    std::cout << "Hello from a lambda thread!" << std::endl;
});

std::cout << ">>> Main thread ending" << std::endl;

Join vs Detach

Thread lifecycle management determines whether:

  • the main thread waits
  • or lets the worker run independently

join() — “Don’t leave until the job is done”

std::thread prep_cook(prepare_mise_en_place);
prep_cook.join();

Use join() when later code depends on the result, correctness matters and when you need confirmation the task finished. This is synchronous coordination.

detach() — “Work independently” (Background Work)

void detached_thread_function() {
    int counter = 1;
    while (true) {
        std::cout << "Detached thread is working... "
                  << counter++ << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
}

std::thread t4(detached_thread_function);
t4.detach();

This thread runs independently, and continues while main() is alive. It stops automatically when the program exits. Use detach() when the task runs in the background, results are not needed and when it’s truly fire-and-forget. This is asynchronous execution.

Every thread must be either joined or detached before destruction.

Passing Objects to Threads

Pass by Value — Safest Default

void cook_pasta(std::vector<std::string> ingredients) {
    ingredients.push_back("parmesan");
}

std::vector<std::string> pantry = {"pasta", "tomatoes"};
std::thread t(cook_pasta, pantry);
t.join();

Each thread gets its own copy. No race conditions. No lifetime issues.

Pass by Reference — Shared State

void update_inventory(std::atomic<int>& count) {
    count.fetch_add(-2);
}

std::atomic<int> tomato_count{50};
std::thread t(update_inventory, std::ref(tomato_count));
t.join();

Shared access requires atomics, mutexes and strict lifetime guarantees.

Dangerous Pattern: Referencing Local Data

void dangerous_example() {
    std::vector<std::string> local_items = {"flour", "eggs"};

    std::thread baker([&local_items]() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        for (auto& item : local_items) {
            std::cout << item << std::endl;
        }
    });

    baker.detach();
} // local_items destroyed → crash

Detached threads + local references = undefined behavior.

Promise & Future: Returning Results from Threads

Sometimes a thread must compute something and send the result back. This is where std::promise and std::future shine. What are they?

  • std::promise<T> : a container to set a value
  • std::future<T> : a handle to retrieve that value

If one thread produces, another thread consumes. Let’s see the real example : Daily Revenue Calculation

#include <future>
#include <thread>
#include <vector>

struct Order {
    double amount;
};

void calculate_daily_revenue(
    const std::vector<Order>& orders,
    std::promise<double> revenue_promise
) {
    double total = 0.0;
    for (const auto& order : orders) {
        total += order.amount;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    revenue_promise.set_value(total);
}

int main() {
    std::vector<Order> orders = {{12.5}, {20.0}, {7.5}};

    std::promise<double> promise;
    std::future<double> future = promise.get_future();

    std::thread accountant(
        calculate_daily_revenue,
        orders,
        std::move(promise)
    );

    double revenue = future.get(); // waits automatically
    std::cout << "Today's revenue: $" << revenue << std::endl;

    accountant.join();
}

What’s Happening Step by Step?

  1. Main thread creates a promise
  2. Main thread gets a future connected to it
  3. Worker thread calculates revenue
  4. Worker calls set_value()
  5. future.get() unblocks and receives the value

No mutex. No shared memory. No race conditions.

When in doubt, pass by value. Use references only when shared state is necessary and safe. And remember that threads are easy to create. Correct thread management is what separates demos from production code.

Final Takeaway

Concurrency is powerful — but unforgiving.

  • Always manage thread lifetimes
  • Be explicit with join() or detach()
  • Respect data ownership and lifetime
  • Use promise/future for clean result passing

Solidifying these fundamentals makes modern, responsive C++ applications possible.


메타데이터
post_id
ccee82cc87fb
slug
understanding-threads-in-c-ccee82cc87fb
url
https://medium.com/@su-paris/understanding-threads-in-c-ccee82cc87fb
canonical_url
https://medium.com/@su-paris/understanding-threads-in-c-ccee82cc87fb
author_url
https://medium.com/@su-paris
status
ok
fetched_at
2026-07-13 06:23:13