Как std::jthread организует кооперативную остановку рабочего потока?
std::jthread не прерывает поток принудительно: он передаёт ему std::stop_token, а завершение выполняется кооперативно. При уничтожении joinable-объекта std::jthread автоматически запрашивает остановку и ожидает завершения потока, поэтому функция потока должна периодически проверять запрос или ожидать с поддержкой токена.
У std::thread управление временем жизни разделено между программистом: перед уничтожением объекта нужно вызвать join или detach. Ошибка в этом управлении приводит к аварийному завершению программы, а сам std::thread не содержит стандартного механизма кооперативной отмены.
В C++20 появился std::jthread, объединивший автоматическое присоединение потока с переносимым механизмом запроса остановки. Это решает задачу безопасного завершения фоновых операций без принудительного прерывания их выполнения.
Поток может выполнять долгую работу, поэтому простого вызова запроса остановки недостаточно: поток должен сам его обнаружить. Если функция игнорирует stop_token, деструктор std::jthread всё равно будет ждать её завершения.
Принудительное прерывание опасно: поток может находиться между изменениями общего состояния, владеть ресурсом или выполнять операцию, которую нельзя безопасно прервать. Поэтому стандарт предоставляет именно уведомление о намерении остановиться, а не механизм уничтожения потока.
std::jthread владеет потоком и связанным с ним состоянием остановки. Метод request_stop() устанавливает запрос, после чего stop_token в рабочем потоке начинает сообщать об остановке через stop_requested().
Если вызываемый объект принимает std::stop_token первым параметром, std::jthread передаёт ему токен автоматически:
При выходе из main деструктор thread запрашивает остановку и выполняет join. Цикл замечает запрос и завершается; после этого ожидание в деструкторе заканчивается.
Запрос остановки является потокобезопасным и идемпотентным по смыслу: несколько вызовов не создают несколько различных состояний остановки. Проверка токена должна находиться в разумных точках: между итерациями, после завершения части работы или внутри ожидающей операции.
Для блокирующих ожиданий полезны перегрузки стандартных средств, поддерживающие токен, например condition_variable_any::wait с std::stop_token. Обычный вызов condition_variable::wait сам по себе от запроса остановки не просыпается, поэтому потребуется отдельное уведомление или другой протокол завершения.
Главный компромисс — автоматическое ожидание повышает безопасность времени жизни, но может задержать уничтожение объекта на неопределённо долгое время, если рабочая функция зависла или не проверяет токен. std::jthread не делает небезопасные операции отменяемыми автоматически.
Сервис запускает поток, который периодически проверяет очередь задач. Вариант с std::thread требует вручную хранить флаг остановки, будить поток, корректно вызвать join на всех путях выхода и не допустить уничтожения объектов, которыми поток ещё пользуется. Ошибка в одном из этих шагов может привести к зависанию, гонке или аварийному завершению.
Вариант с std::jthread и std::stop_token делает владение потоком RAII-совместимым и явно описывает протокол завершения. Если поток спит через ожидание, поддерживающее токен, запрос остановки одновременно будит его; если он выполняет короткие итерации, достаточно периодической проверки.
Выбранное решение — std::jthread, потому что фоновой операции нужна именно кооперативная остановка, а её владелец должен гарантированно дождаться завершения. Результат: при штатном выходе поток освобождает свои ресурсы до уничтожения связанных объектов, но зависание всё ещё требует исправить саму рабочую функцию или её ожидания.
Прекращает ли request_stop() выполнение функции немедленно?
Нет. Метод только устанавливает запрос и может запустить зарегистрированные callbacks остановки. Рабочий поток должен сам проверить токен, выйти из цикла или отреагировать через поддерживающее токен ожидание. Если функция заблокирована в неподходящем системном вызове и не получает отдельного сигнала, join может ждать бесконечно.
Что произойдёт, если передать в std::jthread функцию без std::stop_token?
Поток всё равно будет запущен, если вызываемый объект вызываем без токена. Однако функция не получит удобного аргумента для проверки запроса и должна использовать другой механизм уведомления, например собственный объект состояния. Автоматическое присоединение при уничтожении сохранится, но кооперативная остановка сама по себе работать не будет.
Безопасно ли уничтожать std::jthread, если рабочая функция ещё обращается к локальным данным владельца?
Да, только если данные живут дольше потока. Деструктор std::jthread сначала запрашивает остановку, затем ждёт завершения joinable-потока, поэтому локальные объекты, объявленные до std::jthread, обычно остаются живы до окончания его деструктора. Но объект, захваченный потоком по ссылке и уничтоженный раньше самого std::jthread, всё равно создаёт ошибку времени жизни; jthread не исправляет неправильные захваты и не продлевает время жизни произвольных объектов.