std::thread, std::jthread & Thread Management
Научитесь создавать и управлять потоками с помощью std::thread и C++20. Поймите, что нужно объединиться против отсоединения, отменить сотрудничество с токенами остановки, локальным хранилищем потоков и обработкой исключений через границы потоков.
Почему параллелизм?
Современные процессоры имеют несколько ядер, и для их использования ваша программа должна работать одновременно. C++11 представила портативную библиотеку потоков в , а C++20 улучшила ее с помощью std::jthread. thread - это независимый путь исполнения, который разделяет то же адресное пространство, что и созданная им нить, которая является одновременно его силой и опасностью.
В отличие от процессов, потоки разделяют память. Это делает связь быстрой, но также вводит расы данных, если два потока читают и пишут одну и ту же переменную без синхронизации. Прежде чем мы приступим к синхронизации (следующий урок), давайте сначала узнаем, как создавать, управлять и отключать потоки.
Создание Threads с помощью std::thread
Вы создаете std::thread, передавая ему вызов (функцию, лямбду, функтор) и любые аргументы. Нить начинает выполняться немедленно при строительстве.
#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;
}Присоединиться() vs отсоединить()
Каждый std::thread, представляющий собой нить, должен быть либо присоединен, либо отсоединен перед запуском деструктора. Если вы забыли, std::terminate() вызывается и ваша программа падает.
join() блокирует вызывающий поток до тех пор, пока не закончится целевой поток. Это безопасный дефолт — вы знаете, что поток завершен и его результаты готовы.
detach() разрывает связь между объектом std::thread и потоком ОС. Нить проходит в фоновом режиме («нить демона»), и вы больше не можете к ней присоединиться. Будьте осторожны: если отсоединенный поток получает доступ к переменным стека из функции нереста, и эта функция возвращается, вы получаете * * неопределенное поведение * *.
Никогда не detach() нить, которая захватывает или ссылается на локальные переменные из области создания. Когда функция создания возвращается, эти локальные элементы разрушаются, и отсоединенная нить получает доступ к висящей памяти. Это неопределенное поведение и часто проявляется как спорадические сбои. Если вы должны отсоединить, передайте данные по значению или используйте std::shared_ptr для продления срока службы.
RAII Thread (C++20)
std::jthread решает две проблемы с std::thread: (1) он автоматически присоединяется к своему деструктору, поэтому вы никогда не можете забыть, и (2) он поддерживает совместную отмену через std::stop_token.
#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;
}Запрос оборудования и локального хранилища Thread
std::thread::hardware_concurrency() возвращает количество параллельных потоков, поддерживаемых аппаратным обеспечением. Длительность хранения thread_local дает каждому потоку собственную копию переменной.
#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.
Исключение - обработка через нити
Исключение, брошенное внутри нити, которая не поймана в этой нити, называется std::terminate(). Исключения распространяются не автоматически на соединительную нить.
Для переноса исключений между потоками улавливайте их с помощью std::current_exception() внутри рабочего потока и храните их в std::exception_ptr. Затем соединительная нить может быть восстановлена с помощью std::rethrow_exception(). Механизм std::future (урок 4) обрабатывает это автоматически.
std::threadначинает выполняться немедленно; вы должны запуститьjoin()илиdetach()до запуска деструктора.std::jthread(C++20) автоматически присоединяется к своему деструктору и поддерживает отмену сотрудничества черезstd::stop_tokenthread_localпредоставляет каждому потоку свою собственную независимую копию переменной — синхронизация не требуется.std::thread::hardware_concurrency()расскажет вам, сколько потоков может работать одновременно- Исключения в потоке не распространяются на абонента — используйте
std::exception_ptrилиstd::futureдля их передачи. - Никогда не позволяйте отдельной нити ссылаться на локальные переменные из области нереста.
Тест — проверьте свои знания
(15 XP)1. Что произойдет, если объект `std::thread`, представляющий соединительную нить, будет уничтожен без вызова `join()` или `detach()`?
2. Как `std::jthread` поддерживает отмену сотрудничества?
3. Что делает ключевое слово `thread_local`?