Threads and Basic Execution Paths
Advanced Concurrency, SIMD, and Performance

9.1 Threads and Basic Execution Paths

In C++, concurrency allows your application to execute multiple tasks simultaneously, utilizing all available CPU cores. While older languages run within single threaded execution models or hide threads behind virtual runtimes, C++ gives you direct control over hardware threads.

Let's look at basic thread spawning, stack frames, and thread destruction rules.

OS Schedulers and CPU Context Switches

When you create a std::thread, you are directly invoking an Operating System system call (like clone on Linux or CreateThread on Windows) to create an OS level thread.

The OS Thread Scheduler controls execution. It assigns a CPU core to your thread for a few milliseconds (a time slice or quantum). When time is up, the OS rips the CPU away, saves your thread's registers to RAM, loads another thread's registers, and resumes execution. This violent swapping is called a Context Switch, and it is expensive. Spawning 10,000 raw threads will crush your CPU under the weight of context switching.

#include <iostream>
#include <thread>

void printMessage() {
    std::cout << "Hello from thread: " << std::this_thread::get_id() << '\n';
}

int main() {
    std::cout << "Main thread: " << std::this_thread::get_id() << '\n';

    // Spawn a new OS execution thread
    std::thread worker{printMessage};

    // Wait for the worker thread to finish before exiting main!
    worker.join();
    return 0;
}
Spawning OS hardware threads.

Thread Lifetime Rules: Join vs Detach

When you instantiate a std::thread object, it begins executing immediately. However, you must explicitly manage its lifetime. If the thread object goes out of scope and you have not decided how to handle it, the destructor of std::thread will call std::terminate, crashing your entire application immediately.

You have two options to manage thread lifetimes:

  • join(): This blocks the current thread (usually the main thread) and forces it to wait until the worker thread completes its function. This is the safest way to ensure thread tasks finish before resources are cleared.
  • detach(): This separates the thread execution path from the thread object. The thread runs independently in the background, managed directly by the operating system. You cannot join a detached thread later.
  • The Assistant Metaphor: Spawning a thread is like hiring an assistant to run an errand for you (like buying groceries) while you stay home. If you lock the front door and go to sleep (let the main function exit) before waiting for the assistant to return (calling join()), the assistant is locked out and the task fails, crashing the system. Detaching is like sending the assistant to deliver flyers. They run on their own schedule, and you do not wait for them.

Modern C++20 Joining Threads: `std::jthread`

Because forgetting to call join() or detach() is a common source of crashes, C++20 introduced std::jthread (joining thread). It acts as a safe wrapper around standard threads:

  • Auto Join: In its destructor, std::jthread calls request_stop() (when a stop token is in use) and then join(), preventing terminate crashes.
  • Cooperative Cancellation: It supports stop tokens, allowing you to check if a thread has been requested to stop and exit cleanly:
#include <iostream>
#include <thread>
#include <stop_token>
#include <chrono>

void worker(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::cout << "Worker running...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    std::cout << "Worker exiting cleanly!\n";
}

int main() {
    {
        std::jthread jt{worker};
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
        // jt destructor runs here! It requests stop and joins automatically!
    }
    return 0;
}
Safe execution using std::jthread and stop tokens.
Finished reading this lesson?