What Are Joining and Detaching?

Every std::thread must be either joined or detached before it is destroyed. Joining waits for the thread to finish, while detaching lets the thread run independently in the background. Failing to do either causes std::terminate() to be called.

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:

  1. Join - Wait for the thread to complete
  2. Detach - Let the thread run independently
If you do neither, your program calls `std::terminate()` and crashes.
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::thread object 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
Notice the background thread may not finish - when `main()` returns, the program exits and detached threads are terminated abruptly.

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

Detaching is inherently risky. Consider the following problems carefully before using `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
**Best practice:** Default to `join()`. Use `detach()` only when you have a specific reason and have carefully considered the lifetime issues.

Summary

  • join() blocks until the thread completes; use it to wait for results and ensure cleanup
  • detach() lets threads run independently but sacrifices control and safety
  • Every std::thread must 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() over detach() unless you have a compelling reason