Перейти к содержимому

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

Научитесь создавать и управлять потоками с помощью std::thread и C++20. Поймите, что нужно объединиться против отсоединения, отменить сотрудничество с токенами остановки, локальным хранилищем потоков и обработкой исключений через границы потоков.

Почему параллелизм?

Современные процессоры имеют несколько ядер, и для их использования ваша программа должна работать одновременно. C++11 представила портативную библиотеку потоков в , а C++20 улучшила ее с помощью std::jthread. thread - это независимый путь исполнения, который разделяет то же адресное пространство, что и созданная им нить, которая является одновременно его силой и опасностью.

В отличие от процессов, потоки разделяют память. Это делает связь быстрой, но также вводит расы данных, если два потока читают и пишут одну и ту же переменную без синхронизации. Прежде чем мы приступим к синхронизации (следующий урок), давайте сначала узнаем, как создавать, управлять и отключать потоки.

Создание Threads с помощью std::thread

Вы создаете std::thread, передавая ему вызов (функцию, лямбду, функтор) и любые аргументы. Нить начинает выполняться немедленно при строительстве.

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;
}

Присоединиться() 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.

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;
}

Запрос оборудования и локального хранилища Thread

std::thread::hardware_concurrency() возвращает количество параллельных потоков, поддерживаемых аппаратным обеспечением. Длительность хранения thread_local дает каждому потоку собственную копию переменной.

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.

Исключение - обработка через нити

Исключение, брошенное внутри нити, которая не поймана в этой нити, называется std::terminate(). Исключения распространяются не автоматически на соединительную нить.

Для переноса исключений между потоками улавливайте их с помощью std::current_exception() внутри рабочего потока и храните их в std::exception_ptr. Затем соединительная нить может быть восстановлена с помощью std::rethrow_exception(). Механизм std::future (урок 4) обрабатывает это автоматически.

Ключевые выводы
  • std::thread начинает выполняться немедленно; вы должны запустить join() или detach() до запуска деструктора.
  • 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`?