Skip to content

67 Intermediate The Hundred

The Thread You Forgot to Join

the abort that nothing threw

void job(const char* name, int ms) {
    std::this_thread::sleep_for(std::chrono::milliseconds(ms));
    std::cout << name << " finished" << std::endl;
}

void nightly_maintenance() {
    std::thread worker(job, "compression", 20);   // compress in the background
    job("index rotation", 100);                   // meanwhile, rotate the index
}

int main() {
    nightly_maintenance();
    std::cout << "maintenance complete" << std::endl;
}

Run it. Both jobs report success — so what's the exit status?

Answer

134. Typical output (GCC 11.5 on x86-64 Linux):

compression finished
index rotation finished
terminate called without an active exception

The shell adds its own Aborted notice; maintenance complete never prints. (The std::endl is load-bearing: abort doesn't drain a buffered stream, so with plain "\n" and stdout redirected to a file you'd see the terminate line alone.)

Why

At the closing brace of nightly_maintenance, worker is destroyed while still joinable — nobody called join() or detach() — and ~thread answers that by calling std::terminate. This is not undefined behavior; the standard mandates it. The committee had no safe default to fall back on: joining silently would let a destructor block for an unbounded time (forever, on a thread that never returns), and detaching silently would let the worker keep touching locals the exiting scope has just destroyed — so it made the omission loud instead of guessing wrong. Note that the worker had already finished its work: joinable describes the handle, not the thread's progress, and a completed thread stays joinable until someone joins it. An early return, break, or exception between the constructor and a later join() trips the same wire, and GCC 11.5 with -Wall -Wextra warns about none of it.

The fix

Join — or detach — on every path out of the scope, exception paths included. That is a job for RAII, and C++20 ships the handle that already does it:

worker.join();                                // C++17: every path must reach this
std::jthread worker(job, "compression", 20);  // C++20: destructor joins, throw or not

Takeaway: a std::thread must be joined or detached before it goes out of scope — its destructor's only other move is to kill the program.

Try it: g++ -std=c++17 -pthread main.cpp -o demo && ./demo

Open in Compiler Explorer ↗ Quiz this entry