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…
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()vsdetach()- Passing data to threads safely
- Understanding
std::promiseandstd::futurewith 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 valuestd::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?
- Main thread creates a promise
- Main thread gets a future connected to it
- Worker thread calculates revenue
- Worker calls
set_value() 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()ordetach() - Respect data ownership and lifetime
- Use
promise/futurefor 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