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):
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