Μετάβαση στο περιεχόμενο

std::thread, std::jthread & Thread Management

Learn to create and manage threads with std::thread and C++20's std::jthread. Understand join vs detach, cooperative cancellation with stop tokens, thread-local storage, and exception handling across thread boundaries.

Why Concurrency?

Οι σύγχρονες CPU έχουν πολλούς πυρήνες και για να τους εκμεταλλευτείτε το πρόγραμμά σας πρέπει να λειτουργεί ταυτόχρονα. Το C++11 εισήγαγε μια φορητή βιβλιοθήκη νημάτων στοκαι το C++20 τη βελτίωσε με τοstd::jthread. Ένα νήμα είναι μια ανεξάρτητη διαδρομή εκτέλεσης που μοιράζεται τον ίδιο χώρο διευθύνσεων με το νήμα που το δημιούργησε — που είναι και η ισχύς και ο κίνδυνος του.

Σε αντίθεση με τις διεργασίες, τα νήματα μοιράζονται τη μνήμη. Αυτό κάνει την επικοινωνία γρήγορη, αλλά εισάγει επίσης κούρσες δεδομένων εάν δύο νήματα διαβάζουν και γράφουν την ίδια μεταβλητή χωρίς συγχρονισμό. Πριν ασχοληθούμε με το συγχρονισμό (επόμενο μάθημα), ας μάθουμε πρώτα πώς να δημιουργείτε, να διαχειριζόμαστε και να κλείνουμε καθαρά νήματα.

Creating Threads with std::thread

You create astd::threadby passing it a callable (function, lambda, functor) and any arguments. The thread begins executing immediately upon construction.

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

Everystd::threadthat represents a running thread must be either joined or detached before its destructor runs. If you forget,std::terminate()is called and your program crashes.

join() blocks the calling thread until the target thread finishes. This is the safe default — you know the thread is done and its results are ready.

detach() severs the connection between thestd::threadobject and the OS thread. The thread runs in the background (a "daemon thread") and you can no longer join it. Be cautious: if the detached thread accesses stack variables from the spawning function and that function returns, you get undefined behavior.

Παγίδα

Neverdetach()a thread that captures or references local variables from the creating scope. When the creating function returns, those locals are destroyed and the detached thread accesses dangling memory. This is undefined behavior and often manifests as sporadic crashes. If you must detach, pass data by value or usestd::shared_ptrto extend lifetimes.

std::jthread — Το νήμα RAII (C++20)

std::jthreadsolves two problems withstd::thread: (1) it automatically joins in its destructor, so you can never forget, and (2) it supports cooperative cancellation viastd::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;
}

Querying Hardware & Thread-Local Storage

std::thread::hardware_concurrency()returns the number of concurrent threads the hardware supports. Thethread_localstorage duration gives each thread its own copy of a variable.

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;
}
Βέλτιστη πρακτική

Σε C++20 και μεταγενέστερα, προτιμήστε τοstd::jthreadαπό τοstd::thread. Ενώνεται αυτόματα στον καταστροφέα του, εξαλείφοντας τον κίνδυνο να καλέσετε τοstd::terminateεάν ξεχάσετε να εγγραφείτε. Ο ενσωματωμένος μηχανισμόςstd::stop_tokenπαρέχει έναν καθαρό, τυποποιημένο τρόπο για να ζητήσετε ακύρωση συνεργασίας — δεν χρειάζεται να βάλετε τη δική σας σημαίαatomic shouldStop.

Χειρισμός εξαιρέσεων σε όλα τα νήματα

An exception thrown inside a thread that is not caught within that thread callsstd::terminate(). Exceptions do not propagate automatically to the joining thread.

To transfer exceptions between threads, capture them withstd::current_exception()inside the worker thread and store them in astd::exception_ptr. The joining thread can then rethrow withstd::rethrow_exception(). Thestd::futuremechanism (covered in Lesson 4) handles this automatically.

Βασικά συμπεράσματα
  • std::threadstarts executing immediately; you mustjoin()ordetach()before its destructor runs
  • Τοstd::jthread(C++20) ενώνεται αυτόματα στον καταστροφέα του και υποστηρίζει ακύρωση συνεργασίας μέσωstd::stop_token
  • Τοthread_localδίνει σε κάθε νήμα το δικό του ανεξάρτητο αντίγραφο μιας μεταβλητής — δεν χρειάζεται συγχρονισμός
  • Τοstd::thread::hardware_concurrency()σας λέει πόσα νήματα μπορεί να τρέξει το υλικό ταυτόχρονα
  • Οι εξαιρέσεις σε ένα νήμα δεν διαδίδονται στον καλούντα — χρησιμοποιήστεstd::exception_ptrήstd::futureγια να τις μεταφέρετε
  • Μην αφήνετε ποτέ ένα αποκομμένο νήμα να παραπέμπει σε τοπικές μεταβλητές από το πεδίο αναπαραγωγής

Κουίζ — Δοκιμάστε τις γνώσεις σας

(15 XP)

1. Τι συμβαίνει εάν ένα αντικείμενο`std::thread`που αντιπροσωπεύει ένα νήμα που μπορεί να συνδεθεί καταστραφεί χωρίς να καλέσετε το`join()`ή το`detach()`;

2. Πώς υποστηρίζει το`std::jthread`την ακύρωση συνεταιρισμού;

3. Τι κάνει η λέξη-κλειδί`thread_local`;