Что происходит в момент создания std::thread?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что происходит в момент создания 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
}
};
Итоговая последовательность
- Конструктор std::thread создаётся
- Аргументы копируются в потокобезопасное хранилище
- Создаётся type-erased wrapper
- Вызывается pthread_create() (или CreateThread на Windows)
- ОС выделяет stack (~2MB)
- ОС инициализирует TLS для нового потока
- Планировщик ОС переключает контекст на новый поток
- Выполняется thread_wrapper, который вызывает вашу функцию
- Исходный поток не блокируется — продолжает работу
- Новый поток работает параллельно