Zum Inhalt springen

std::thread, std::jthread & Thread-Verwaltung

Lerne, Threads mit std::thread und dem std::jthread aus C++20 zu erzeugen und zu verwalten. Verstehe join vs. detach, kooperativen Abbruch mit Stop-Tokens, thread-lokalen Speicher und die Behandlung von Exceptions über Thread-Grenzen hinweg.

Warum Nebenläufigkeit?

Moderne CPUs haben mehrere Kerne, und um sie auszunutzen, muss dein Programm Arbeit nebenläufig ausführen. C++11 führte mit eine portable Threading-Bibliothek ein, C++20 hat sie um std::jthread erweitert. Ein Thread ist ein eigenständiger Ausführungspfad, der sich denselben Adressraum mit dem Thread teilt, der ihn erzeugt hat — darin liegt zugleich seine Stärke und seine Gefahr.

Anders als Prozesse teilen sich Threads den Speicher. Das macht die Kommunikation schnell, bringt aber auch Data Races mit sich, wenn zwei Threads dieselbe Variable ohne Synchronisation lesen und schreiben. Bevor wir uns der Synchronisation widmen (nächste Lektion), lernen wir zunächst, wie man Threads erzeugt, verwaltet und sauber herunterfährt.

Threads mit std::thread erzeugen

Einen std::thread erzeugst du, indem du ihm ein aufrufbares Objekt (Funktion, Lambda, Funktor) und beliebige Argumente übergibst. Der Thread beginnt sofort bei der Konstruktion mit der Ausführung.

create_thread.cpp
#include <iostream>
#include <thread>

void greet(const std::string& name, int times) {
    for (int i = 0; i < times; ++i) {
        std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
    }
}

int main() {
    // Launch a thread running greet("Alice", 3)
    std::thread t1(greet, "Alice", 3);

    // Lambda-based thread
    std::thread t2([]() {
        std::cout << "Running in a lambda thread\n";
    });

    // MUST join or detach before t1/t2 are destroyed
    t1.join();  // block until t1 finishes
    t2.join();

    std::cout << "All threads finished\n";
    return 0;
}

join() vs. detach()

Jeder std::thread, der einen laufenden Thread repräsentiert, muss vor dem Aufruf seines Destruktors entweder gejoint (join) oder abgekoppelt (detach) werden. Vergisst du das, wird std::terminate() aufgerufen und dein Programm stürzt ab.

join() blockiert den aufrufenden Thread, bis der Ziel-Thread fertig ist. Das ist die sichere Voreinstellung — du weißt, dass der Thread fertig ist und seine Ergebnisse bereitstehen.

detach() trennt die Verbindung zwischen dem std::thread-Objekt und dem Betriebssystem-Thread. Der Thread läuft im Hintergrund weiter (als "Daemon-Thread") und kann nicht mehr gejoint werden. Sei vorsichtig: Greift der abgekoppelte Thread auf Stack-Variablen der erzeugenden Funktion zu und kehrt diese Funktion zurück, erhältst du undefiniertes Verhalten.

Stolperfalle

Rufe niemals detach() für einen Thread auf, der lokale Variablen des erzeugenden Gültigkeitsbereichs einfängt oder referenziert. Kehrt die erzeugende Funktion zurück, werden diese lokalen Objekte zerstört, und der abgekoppelte Thread greift auf freigegebenen Speicher zu. Das ist undefiniertes Verhalten und äußert sich oft in sporadischen Abstürzen. Wenn du abkoppeln musst, übergib die Daten per Wert oder nutze std::shared_ptr, um die Lebensdauer zu verlängern.

std::jthread — der RAII-Thread (C++20)

std::jthread löst zwei Probleme von std::thread: (1) Er joint automatisch in seinem Destruktor, sodass du es nie vergessen kannst, und (2) er unterstützt kooperativen Abbruch über std::stop_token.

jthread_demo.cpp
#include <iostream>
#include <thread>
#include <chrono>

void worker(std::stop_token stoken, int id) {
    while (!stoken.stop_requested()) {
        std::cout << "Worker " << id << " running...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    std::cout << "Worker " << id << " stopping gracefully\n";
}

int main() {
    std::jthread jt1(worker, 1);  // stop_token is auto-passed as first arg
    std::jthread jt2(worker, 2);

    std::this_thread::sleep_for(std::chrono::seconds(1));

    // Request both threads to stop
    jt1.request_stop();
    jt2.request_stop();

    // No need to call join() — jthread's destructor does it
    std::cout << "Main thread exiting\n";
    return 0;
}

Hardware abfragen & thread-lokaler Speicher

std::thread::hardware_concurrency() liefert die Anzahl der Threads, die die Hardware nebenläufig ausführen kann. Die Speicherdauer thread_local gibt jedem Thread seine eigene Kopie einer Variablen.

hardware_tls.cpp
#include <iostream>
#include <thread>
#include <vector>

thread_local int tls_counter = 0;  // each thread gets its own copy

void count_work(int id) {
    for (int i = 0; i < 1000; ++i) {
        ++tls_counter;  // no data race — each thread has its own
    }
    std::cout << "Thread " << id << ": tls_counter = " << tls_counter << "\n";
}

int main() {
    unsigned int n = std::thread::hardware_concurrency();
    std::cout << "Hardware concurrency: " << n << " threads\n";

    std::vector<std::thread> threads;
    for (unsigned int i = 0; i < n; ++i) {
        threads.emplace_back(count_work, i);
    }
    for (auto& t : threads) {
        t.join();
    }
    // Each thread printed 1000 — they don't share tls_counter
    return 0;
}
Best Practice

Ab C++20 solltest du std::jthread gegenüber std::thread bevorzugen. Er joint automatisch in seinem Destruktor und beseitigt damit das Risiko, dass std::terminate aufgerufen wird, weil du das Joinen vergessen hast. Sein eingebauter std::stop_token-Mechanismus bietet einen sauberen, standardisierten Weg, einen kooperativen Abbruch anzufordern — du musst dir kein eigenes atomic shouldStop-Flag mehr basteln.

Exception-Behandlung über Thread-Grenzen hinweg

Eine Exception, die in einem Thread geworfen und innerhalb dieses Threads nicht gefangen wird, führt zum Aufruf von std::terminate(). Exceptions pflanzen sich nicht automatisch zum joinenden Thread fort.

Um Exceptions zwischen Threads zu übertragen, fängst du sie im Worker-Thread mit std::current_exception() ein und speicherst sie in einem std::exception_ptr. Der joinende Thread kann sie dann mit std::rethrow_exception() erneut werfen. Der std::future-Mechanismus (Thema in Lektion 4) erledigt das automatisch.

Wichtigste Erkenntnisse
  • std::thread startet die Ausführung sofort; du musst vor dem Destruktor join() oder detach() aufrufen
  • std::jthread (C++20) joint automatisch im Destruktor und unterstützt kooperativen Abbruch über std::stop_token
  • thread_local gibt jedem Thread seine eigene, unabhängige Kopie einer Variablen — ganz ohne Synchronisation
  • std::thread::hardware_concurrency() sagt dir, wie viele Threads die Hardware nebenläufig ausführen kann
  • Exceptions in einem Thread pflanzen sich nicht zum Aufrufer fort — nutze std::exception_ptr oder std::future, um sie zu übertragen
  • Lass einen abgekoppelten Thread niemals lokale Variablen des erzeugenden Gültigkeitsbereichs referenzieren

Quiz — Teste dein Wissen

(15 XP)

1. Was passiert, wenn ein `std::thread`-Objekt, das einen joinbaren Thread repräsentiert, zerstört wird, ohne dass `join()` oder `detach()` aufgerufen wurde?

2. Wie unterstützt `std::jthread` den kooperativen Abbruch?

3. Was bewirkt das Schlüsselwort `thread_local`?