← Назад к вопросам

Что происходит в момент создания std::thread?

2.0 Middle🔥 111 комментариев
#Многопоточность и синхронизация#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что происходит в момент создания std::thread?

Отличный вопрос на глубокое понимание многопоточности. Описываю шаг за шагом, что происходит на всех уровнях — от API до ОС.

Шаг 1: Конструктор std::thread

std::thread t(my_function, arg1, arg2);

Когда вы создаёте std::thread:

template<class F, class ...Args>
explicit thread(F&& f, Args&&... args);

Конструктор принимает:

  • F — callable object (функция, lambda, functor)
  • Args... — произвольные аргументы

Параметры идеально пересылаются (perfect forwarding).

Шаг 2: Копирование/перемещение аргументов

Важный момент: Аргументы копируются в thread-safe хранилище.

int value = 42;
std::string str = "hello";

std::thread t([](int v, std::string s) {
    std::cout << v << " " << s << std::endl;
}, value, str);

// value и str КОПИРУЮТСЯ внутрь std::thread
value = 100;  // Не влияет на value внутри потока
str = "world"; // Не влияет на str внутри потока

Чтобы передать по ссылке:

std::thread t([](int& v, std::string& s) {
    v = 100;
    s = "modified";
}, std::ref(value), std::ref(str));

t.join();
// Теперь value и str изменены

Шаг 3: Размещение в памяти

Средство std::thread использует type-erased callable wrapper:

class thread {
private:
    // Внутренняя структура, которая хранит функцию и аргументы
    std::unique_ptr<thread_impl> impl;
};

// Упрощённо:
struct thread_impl_base {
    virtual ~thread_impl_base() {}
    virtual void run() = 0;
};

template<class F>
struct thread_impl : thread_impl_base {
    F func;
    std::tuple<Args...> args;  // Сохраняются аргументы
    
    virtual void run() {
        // Вызов функции с распакованными аргументами
    }
};

Всё это выделяется в динамической памяти (heap).

Шаг 4: Вызов системного системного вызова

На Linux: вызывается pthread_create()

int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *),
                   void *arg);

На Windows: вызывается CreateThread()

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

Шаг 5: Выделение стека

ОС выделяет отдельный stack для нового потока:

  • На 32-бит системах: ~1 MB
  • На 64-бит системах: ~2 MB
  • Настраивается через pthread_attr_setstacksize()
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 4 * 1024 * 1024);  // 4 MB
pthread_create(&tid, &attr, thread_func, nullptr);

Шаг 6: Инициализация TLS (Thread Local Storage)

ОС инициализирует Thread Local Storage — область памяти, уникальная для каждого потока:

thread_local int value = 0;  // У каждого потока свой value

std::thread t1([]{ value = 10; });
std::thread t2([]{ value = 20; });

t1.join();
t2.join();
// В main: value всё ещё 0

Шаг 7: Запуск потока

ОС переключает контекст процессора и начинает выполнять thread wrapper:

// Внутри std::thread реализации:
void* thread_wrapper(void* arg) {
    thread_impl_base* impl = static_cast<thread_impl_base*>(arg);
    try {
        impl->run();  // Выполняем пользовательскую функцию
    } catch (...) {
        // Обработка исключений в потоке
        std::terminate();
    }
    delete impl;
    return nullptr;
}

Шаг 8: Блокирует ли создание потока вызывающий поток?

НЕ БЛОКИРУЕТ!

std::cout << "До создания" << std::endl;
std::thread t([]{
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "Из потока" << std::endl;
});
std::cout << "После создания" << std::endl;  // Выведется сразу!
t.join();  // Вот здесь блокируемся

Вывод:

До создания
После создания
Из потока (через 5 секунд)

Полный пример с визуализацией

#include <iostream>
#include <thread>
#include <chrono>

int main() {
    std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl;
    
    std::thread t([]{
        std::cout << "Worker thread ID: " << std::this_thread::get_id() << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    });
    
    std::cout << "Thread ID from main: " << t.get_id() << std::endl;
    
    // t.join();  // Комментируем для демонстрации проблемы
    
    return 0;  // ОШИБКА! Программа завершится, но поток ещё работает!
}
// Нужно вызвать t.join() или t.detach()

Ошибка: забыли join() или detach()

{
    std::thread t(my_function);
}  // ОШИБКА! Деструктор вызовет std::terminate()
   // Причина: поток всё ещё может работать

// Правильно:
{
    std::thread t(my_function);
    t.join();  // Ждём завершения
}  // Теперь безопасно

// Или:
{
    std::thread t(my_function);
    t.detach();  // Отделяем поток (не ждём)
}  // Программа может завершиться до потока

Потенциальные проблемы при создании

1. Недостаток ресурсов:

try {
    std::thread t(func);
} catch (const std::system_error& e) {
    std::cerr << "Failed to create thread: " << e.what() << std::endl;
    // Слишком много потоков, недостаточно памяти
}

2. Race condition при захвате переменных в lambda:

for (int i = 0; i < 10; i++) {
    std::thread t([i]() {  // ПРАВИЛЬНО: захватываем по значению
        std::cout << i << std::endl;
    });
    t.detach();
}

// НЕПРАВИЛЬНО:
for (int i = 0; i < 10; i++) {
    std::thread t([&i]() {  // ОПАСНО: захватываем по ссылке
        std::cout << i << std::endl;  // i может измениться
    });
    t.detach();
}

Производительность создания потока

Создание std::thread — операция дорогая:

  • 1-10 миллисекунд в зависимости от ОС
  • Выделение памяти (стек ~2MB)
  • Системный вызов

Поэтому для массовых параллельных задач лучше использовать thread pool:

#include <thread>
#include <queue>
#include <mutex>

class ThreadPool {
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex task_mutex;
    
public:
    ThreadPool(size_t num_threads) {
        for (size_t i = 0; i < num_threads; i++) {
            workers.emplace_back([this] { worker_loop(); });
        }
    }
    
    void enqueue(std::function<void()> task) {
        {
            std::lock_guard<std::mutex> lock(task_mutex);
            tasks.push(task);
        }
        // Уведомляем worker threads
    }
};

Итоговая последовательность

  1. Конструктор std::thread создаётся
  2. Аргументы копируются в потокобезопасное хранилище
  3. Создаётся type-erased wrapper
  4. Вызывается pthread_create() (или CreateThread на Windows)
  5. ОС выделяет stack (~2MB)
  6. ОС инициализирует TLS для нового потока
  7. Планировщик ОС переключает контекст на новый поток
  8. Выполняется thread_wrapper, который вызывает вашу функцию
  9. Исходный поток не блокируется — продолжает работу
  10. Новый поток работает параллельно