Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Joining and Detaching Threads
Understand thread lifecycle management with join and detach operations.
Joining and Detaching Threads
When you create a std::thread, you establish a connection between your code and a system thread. This connection must be properly managed. Before the std::thread object is destroyed, you must explicitly choose one of two paths:
- Join - Wait for the thread to complete
- Detach - Let the thread run independently
void work() {
std::cout << "Working...\n";
}
int main() {
std::thread t(work);
// If we reach here without join() or detach(), CRASH!
} // std::terminate() called
Joining Threads
join() blocks the calling thread until the target thread completes execution:
#include <iostream>
#include <thread>
#include <chrono>
void long_task() {
std::cout << "Task starting...\n";
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Task complete!\n";
}
int main() {
std::thread t(long_task);
std::cout << "Main: waiting for task...\n";
t.join(); // Blocks here for ~2 seconds
std::cout << "Main: task finished!\n";
return 0;
}
Output:
Main: waiting for task...
Task starting...
Task complete!
Main: task finished!
After join() returns:
- The thread has definitely finished executing
- Any side effects from the thread are visible (synchronization)
- The
std::threadobject becomes non-joinable
You Can Only Join Once
std::thread t(work);
t.join();
t.join(); // ERROR: Throws std::system_error
Always check joinable() if there's any possibility the thread was already joined:
if (t.joinable()) {
t.join();
}
Joining Multiple Threads
When you have multiple threads, you often want to wait for all of them:
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(task, i);
}
// Wait for all threads to complete
for (auto& t : threads) {
t.join();
}
The order you join doesn't affect correctness - join() waits for that specific thread regardless of which completes first.
Detaching Threads
detach() separates the thread of execution from the std::thread object. The thread continues running in the background, but you lose all ability to interact with it.
void background_work() {
for (int i = 0; i < 5; ++i) {
std::cout << "Background: " << i << '\n';
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
int main() {
std::thread t(background_work);
t.detach(); // Thread runs independently
// Main thread continues immediately
std::cout << "Main: continuing...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Main: done\n";
return 0;
}
Possible output:
Main: continuing...
Background: 0
Background: 1
Main: done
When to Use Detach
Detached threads are appropriate for:
- Daemon tasks - Background work that should run for the program's lifetime
- Fire-and-forget operations - Tasks where you don't need the result
- Services - Long-running threads that handle requests
The Dangers of Detach
1. No cleanup guarantee:
void risky() {
std::ofstream file("log.txt");
std::thread t([&file]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
file << "Log entry\n"; // file might be destroyed!
});
t.detach();
} // file destroyed, but thread still references it
2. Abrupt termination:
int main() {
std::thread t([]() {
// Do important work...
std::this_thread::sleep_for(std::chrono::seconds(10));
// Clean up resources...
});
t.detach();
// Main thread finishes quickly
} // Detached thread killed mid-execution!
3. No return value or exception handling:
std::thread t([]() {
throw std::runtime_error("Error!"); // Who catches this?
});
t.detach(); // Exception will call std::terminate()
Making Detached Threads Safer
If you must use detached threads:
1. Don't reference local variables:
// BAD
void bad() {
std::vector<int> data = {1, 2, 3};
std::thread t([&data]() { process(data); });
t.detach();
}
// GOOD - copy or move data into thread
void good() {
std::vector<int> data = {1, 2, 3};
std::thread t([data = std::move(data)]() { process(data); });
t.detach();
}
2. Use shared ownership:
void better() {
auto data = std::make_shared<std::vector<int>>();
std::thread t([data]() { // Shared pointer copied
// data stays alive as long as thread runs
process(*data);
});
t.detach();
}
3. Coordinate shutdown properly:
std::atomic<bool> shutdown_requested{false};
void background_service() {
while (!shutdown_requested) {
// Do work...
}
std::cout << "Service shutting down cleanly\n";
}
int main() {
std::thread t(background_service);
t.detach();
// Do main work...
shutdown_requested = true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Give thread time to notice and clean up
}
Exception Safety
What happens if an exception is thrown before you join?
void dangerous() {
std::thread t(work);
might_throw(); // If this throws...
t.join(); // ...we never reach here
} // t is destroyed while joinable -> terminate()!
RAII Thread Guard
The solution is to ensure join() (or detach()) happens even if an exception is thrown:
class JoiningThread {
std::thread t_;
public:
template<typename... Args>
explicit JoiningThread(Args&&... args)
: t_(std::forward<Args>(args)...) {}
~JoiningThread() {
if (t_.joinable()) {
t_.join();
}
}
// Prevent copying
JoiningThread(const JoiningThread&) = delete;
JoiningThread& operator=(const JoiningThread&) = delete;
// Allow moving
JoiningThread(JoiningThread&&) = default;
JoiningThread& operator=(JoiningThread&&) = default;
std::thread& get() { return t_; }
};
void safe() {
JoiningThread t(work);
might_throw(); // Even if this throws...
// ...destructor will join
}
try-catch Approach
Alternatively, use try-catch:
void alternative_safe() {
std::thread t(work);
try {
might_throw();
} catch (...) {
t.join(); // Join even on exception
throw; // Re-throw the exception
}
t.join();
}
This is more verbose and error-prone than RAII, but sometimes necessary.
Checking Thread State
joinable()
Returns true if the thread can be joined or detached:
std::thread t;
std::cout << t.joinable() << '\n'; // false (default constructed)
t = std::thread(work);
std::cout << t.joinable() << '\n'; // true
t.join();
std::cout << t.joinable() << '\n'; // false (joined)
After Moving
std::thread t1(work);
std::cout << t1.joinable() << '\n'; // true
std::thread t2 = std::move(t1);
std::cout << t1.joinable() << '\n'; // false (moved from)
std::cout << t2.joinable() << '\n'; // true (now owns thread)
t2.join();
Choosing Between Join and Detach
Use join() when... |
Use detach() when... |
|---|---|
| You need the thread's result | You don't need the result |
| Thread uses local/scoped resources | Thread is fully self-contained |
| You need to know when it's done | Fire-and-forget operation |
| You want exception propagation | Long-running background task |
| Most of the time! | Rarely, and with caution |
Summary
join()blocks until the thread completes; use it to wait for results and ensure cleanupdetach()lets threads run independently but sacrifices control and safety- Every
std::threadmust be joined or detached before destruction - Use RAII wrappers to ensure threads are joined even when exceptions occur
joinable()tells you if a thread can still be joined or detached- Prefer
join()overdetach()unless you have a compelling reason
Joining and Detaching Threads - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!