многопоточностьconcurrency

std::jthread + std::stop_token

std::jthread ("joining thread") — обёртка над std::thread, чей деструктор сам вызывает request_stop() + join(). std::stop_token/stop_source/stop_callback — стандартизированный механизм кооперативной отмены потока.std::jthread ("joining thread") — a wrapper over std::thread whose destructor calls request_stop() + join() on its own. std::stop_token/stop_source/stop_callback — a standardized mechanism for cooperative thread cancellation.

закрывает класс багов "забыл join()"closes the "forgot join()" bug class отмена кооперативная, не принудительнаяcancellation is cooperative, not preemptive частый вопрос: чем jthread отличается от thread под капотомcommon question: how jthread differs from thread under the hood

Что этоWhat it is

std::jthread — обёртка над std::thread из <thread>, добавляющая ровно одну вещь: деструктор, который, если поток всё ещё joinable, сам вызывает request_stop(), а затем join(). std::stop_token/std::stop_source/std::stop_callback из <stop_token> — независимый, но тесно связанный механизм: функция потока может принимать std::stop_token первым параметром, а тело потока в цикле проверяет token.stop_requested() и завершается само, увидев запрос остановки.std::jthread is a wrapper over std::thread from <thread> that adds exactly one thing: a destructor that, if the thread is still joinable, calls request_stop() itself, followed by join(). std::stop_token/std::stop_source/std::stop_callback from <stop_token> is an independent but tightly related mechanism: a thread function can take a std::stop_token as its first parameter, and the thread body loops checking token.stop_requested(), finishing on its own once it sees the stop request.

Это подробно разобрано ещё в cpp11/thread.html (там же — «Своя реализация» thread pool, которая станет проще именно с jthread) и в cpp_concurrency.md (таблица thread vs jthread, путь std::thread → живой поток ОС через clone()). Здесь — краткая выжимка происхождения и механики, без повторения всего материала.This is covered in detail already in cpp11/thread.html (which also has the "Rolling your own" thread pool section, which gets simpler with jthread) and in cpp_concurrency.md (the thread vs jthread table, the path from std::thread to a live OS thread via clone()). Here is a short summary of the feature's origin and mechanics, without repeating all of that material.

C++11 — свой atomic<bool>-флаг + ручной joinC++11 — a hand-rolled atomic<bool> flag + manual join
std::atomic<bool> stop_flag{false};
std::thread t([&] {
    while (!stop_flag.load()) { /* работа *//* work */ }
});
// ... нужно не забыть:// ... mustn't forget:
stop_flag = true;
t.join();   // забыл — std::terminate в деструкторе thread// forget this — std::terminate in thread's destructor
C++20 — jthread + stop_token
std::jthread t([](std::stop_token st) {
    while (!st.stop_requested()) { /* работа *//* work */ }
});
// деструктор t сам вызовет request_stop() + join()// t's destructor calls request_stop() + join() on its own

Зачем ввелиWhy it was introduced

01

Забытый join()/detach() — постоянный источник std::terminateA forgotten join()/detach() — a persistent source of std::terminate

Разобрано подробно в cpp11/thread.html (раздел «Грабли»): если объект std::thread уничтожается joinable — программа падает через std::terminate, а не исключение, которое можно поймать. jthread устраняет этот класс багов целиком, беря join() на себя.Covered in detail in cpp11/thread.html ("Gotchas" section): if a std::thread object is destroyed while still joinable, the program crashes via std::terminate — not an exception you could catch. jthread eliminates this entire bug class by taking join() on itself.

02

Отмена потока не имела стандартного механизма до C++20Thread cancellation had no standard mechanism before C++20

Каждый проект писал свой atomic<bool>-флаг вручную (см. пример выше и cpp_concurrency.md, раздел "чего избегать") — работающий паттерн, но без единого стандартного имени и API, что затрудняло переиспользование между проектами и требовало повторного объяснения на каждом код-ревью.Every project hand-rolled its own atomic<bool> flag (see the example above, and the "what to avoid" section of cpp_concurrency.md) — a working pattern, but with no single standard name or API, which made reuse across projects harder and required re-explaining it at every code review.

Как это работает под капотомHow it works under the hood

jthread — чисто библиотечная RAII-обёртка, не отдельная модель исполнения: под капотом тот же нативный поток ОС, что и у std::thread (на Linux — тот же путь через pthread_create() → clone() с CLONE_VM, разобранный в cpp_concurrency.md). jthread не создаёт ничего похожего на "зелёный" или "лёгкий" поток — экономия исключительно в том, что RAII-обёртка сама делает то, что раньше приходилось писать вручную в деструкторе окружающего объекта.jthread is a purely library-level RAII wrapper, not a separate execution model: under the hood it's the same native OS thread as std::thread (on Linux, the same path through pthread_create() → clone() with CLONE_VM, covered in cpp_concurrency.md). jthread doesn't create anything resembling a "green" or "lightweight" thread — the savings come purely from an RAII wrapper doing automatically what previously had to be hand-written in the destructor of some surrounding object.

#include <thread>
#include <stop_token>

std::jthread t([](std::stop_token st) {
    while (!st.stop_requested()) {
        // работа, периодически проверяющая токен// work that periodically checks the token
    }
});
// при выходе из scope: t.request_stop(); t.join(); — автоматически// on leaving scope: t.request_stop(); t.join(); — automatically

stop_source — та сторона, что вызывает request_stop(); stop_token — та сторона, что проверяет stop_requested(); оба ссылаются на общее разделяемое состояние (похоже по духу на shared_ptr — увеличивается refcount, а не копируется флаг). stop_callback позволяет зарегистрировать функцию, вызываемую в момент request_stop(), а не только опрашивать флаг в цикле — полезно, если поток заблокирован на операции, которая сама не проверяет токен, но предоставляет способ её прервать через callback.stop_source is the side that calls request_stop(); stop_token is the side that checks stop_requested(); both reference shared underlying state (similar in spirit to shared_ptr — a refcount is incremented, not a flag copied). stop_callback lets you register a function invoked at the moment of request_stop(), rather than only polling a flag in a loop — useful when a thread is blocked on an operation that doesn't itself check the token, but offers a way to interrupt it via a callback.

Именно эту роль решает std::stop_callback в паре с condition_variable: поток спит на cv.wait() и не проснётся сам по себе только оттого, что кто-то вызвал request_stop() — нужно явно разбудить его. Регистрация stop_callback позволяет сделать это автоматически, без ручного протаскивания дополнительного флага "разбудить сейчас" через мьютекс.This is exactly the role std::stop_callback plays alongside a condition_variable: a thread sleeping on cv.wait() won't wake up on its own just because someone called request_stop() — it needs to be explicitly woken. Registering a stop_callback does this automatically, without manually threading an extra "wake up now" flag through a mutex.

#include <condition_variable>
#include <mutex>

std::mutex m;
std::condition_variable cv;
bool data_ready = false;

std::jthread worker([&](std::stop_token st) {
    std::stop_callback wake_on_stop(st, [&] {
        cv.notify_all();   // вызывается автоматически в момент request_stop()// invoked automatically at the moment of request_stop()
    });

    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [&] { return data_ready || st.stop_requested(); });
    // без stop_callback поток спал бы здесь до data_ready,// without the stop_callback the thread would sleep here until data_ready,
    // не замечая request_stop() до случайного следующего пробуждения// not noticing request_stop() until some unrelated next wakeup
});

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Периодическая фоновая задача (heartbeat), останавливаемая без ручного флагаA periodic background task (heartbeat) stopped with no manual flag

Классический фоновый поллинг — раз в секунду отправить heartbeat на сервер — останавливается автоматически в деструкторе jthread, без отдельного atomic<bool> и без риска забыть join().A classic background poll — send a heartbeat to the server once a second — stops automatically in jthread's destructor, with no separate atomic<bool> and no risk of forgetting join().

std::jthread heartbeat([](std::stop_token st) {
    while (!st.stop_requested()) {
        send_heartbeat();
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
});
// когда heartbeat выходит из scope — остановка и join() происходят сами// when heartbeat goes out of scope — stop and join() happen on their own
02

Несколько jthread на одном общем stop_sourceSeveral jthreads sharing one stop_source

Пул воркеров, которые нужно остановить одновременно одним сигналом — общий stop_source создаётся отдельно, а каждому потоку передаётся токен от него, а не собственный (который jthread создал бы себе сам по умолчанию).A pool of workers that all need to be stopped at once with a single signal — a shared stop_source is created separately, and each thread is given a token derived from it, rather than its own (which jthread would otherwise create for itself by default).

std::stop_source shared_stop;
std::vector<std::jthread> workers;

for (int i = 0; i < 4; ++i) {
    workers.emplace_back([&](std::stop_token) {
        while (!shared_stop.get_token().stop_requested()) { /* работа *//* work */ }
    }, shared_stop.get_token());
}

shared_stop.request_stop();   // один вызов — останавливает все 4 воркера разом// one call — stops all 4 workers at once
03

jthread с таймаутом через stop_token + condition_variable::wait_forA jthread with a timeout via stop_token + condition_variable::wait_for

Задача, которая должна либо дождаться события, либо сама прерваться по таймауту, либо быть прервана извне через stop_token — все три исхода проверяются одним wait_for с предикатом.A task that must either wait for an event, time out on its own, or be interrupted externally via a stop_token — all three outcomes are checked by a single wait_for with a predicate.

std::jthread wait_with_timeout([&](std::stop_token st) {
    std::stop_callback wake(st, [&] { cv.notify_all(); });
    std::unique_lock<std::mutex> lk(m);
    bool got_event = cv.wait_for(lk, std::chrono::seconds(5), [&] {
        return event_ready || st.stop_requested();
    });
    if (st.stop_requested()) { /* прервано извне *//* interrupted from outside */ }
    else if (!got_event) { /* истёк таймаут в 5 секунд *//* the 5-second timeout expired */ }
});
04

Было/стало: atomic<bool>-флаг из cpp11/thread.html против jthread + stop_tokenBefore/after: the atomic<bool> flag from cpp11/thread.html vs jthread + stop_token

В cpp11/thread.html (раздел «Грабли») забытый join()/detach() описан как постоянный источник std::terminate именно потому, что std::thread не даёт стандартного способа ни остановить, ни гарантированно присоединить поток — из-за этого проекты вручную заводили свой atomic<bool>-флаг как обходной путь для отмены (см. пример "до" в самом верху этой страницы). jthread + stop_token закрывают ровно эту пару проблем стандартными средствами одновременно — и остановку, и присоединение.In cpp11/thread.html ("Gotchas" section), a forgotten join()/detach() is described as a persistent source of std::terminate precisely because std::thread offers no standard way to either stop or reliably join a thread — which is why projects hand-rolled their own atomic<bool> flag as a cancellation workaround (see the "before" example at the very top of this page). jthread + stop_token close exactly that pair of problems with standard tooling at once — both stopping and joining.

// было — C++11, как в cpp11/thread.html: свой флаг + риск забыть join()// before — C++11, as in cpp11/thread.html: a hand-rolled flag + the risk of forgetting join()
std::atomic<bool> stop_flag{false};
std::thread worker([&] {
    while (!stop_flag.load()) { poll_once(); }
});
// ... позже, и не дай бог забыть эти две строки:// ... later, and heaven forbid forgetting these two lines:
stop_flag = true;
worker.join();

// стало — C++20: остановка и join гарантированы деструктором// after — C++20: stop and join are guaranteed by the destructor
std::jthread worker([](std::stop_token st) {
    while (!st.stop_requested()) { poll_once(); }
});
// ничего вызывать не нужно — request_stop()+join() в деструкторе worker// nothing to call manually — request_stop()+join() live in worker's destructor

ГраблиGotchas

jthread не меняет модель исполнения ОС — не путать с "лёгким" потокомjthread doesn't change the OS execution model — don't confuse it with a "lightweight" thread

Под капотом всё тот же нативный поток (см. cpp11/thread.html про clone()/pthread) — jthread не даёт ни меньшего расхода памяти на стек, ни более дешёвого переключения контекста, ни какой-либо формы "зелёных" потоков. Единственная разница с std::thread — библиотечное RAII-поведение деструктора.Under the hood it's still the same native thread (see cpp11/thread.html on clone()/pthread) — jthread doesn't give you a smaller stack memory footprint, cheaper context switching, or any form of "green" threading. The only difference from std::thread is the destructor's library-level RAII behavior.

stop_requested() — кооперативная отмена, не принудительнаяstop_requested() is cooperative cancellation, not preemptive

Поток, не проверяющий token явно, не остановится сам по себе — в отличие, например, от pthread_cancel (которого в стандартном C++ нет вовсе), request_stop() не прерывает исполнение принудительно. Если тело потока не содержит явных проверок stop_requested() в цикле (например, застряло на блокирующем системном вызове без stop_callback), request_stop() не имеет никакого практического эффекта, пока поток сам не дойдёт до следующей проверки — которой может не случиться вовсе.A thread that doesn't explicitly check the token won't stop on its own — unlike, say, pthread_cancel (which doesn't exist in standard C++ at all), request_stop() doesn't forcibly interrupt execution. If the thread body has no explicit stop_requested() checks in its loop (say, it's stuck on a blocking system call with no stop_callback registered), request_stop() has no practical effect until the thread reaches its next check on its own — which might never happen.