многопоточностьconcurrency
Первая портируемая многопоточность в стандарте C++ — тонкая обёртка над нативными потоками ОС (pthread на Linux). До C++11 многопоточность существовала только через платформенные API или сторонние библиотеки вроде Boost.Thread.The first portable concurrency support in standard C++ — a thin wrapper over native OS threads (pthread on Linux). Before C++11, concurrency existed only through platform-specific APIs or third-party libraries such as Boost.Thread.
std::thread — класс из <thread>, представляющий отдельный поток исполнения. Конструктор запускает переданную функцию (или лямбду, функтор) в новом потоке немедленно; join() блокирует вызывающий поток до завершения; detach() отсоединяет поток, позволяя ему исполняться независимо от объекта std::thread.std::thread is a class from <thread> representing a single thread of execution. The constructor starts the passed function (or lambda, functor) in a new thread immediately; join() blocks the calling thread until it finishes; detach() detaches the thread, letting it run independently of the std::thread object.
pthread_t t;
pthread_create(&t, nullptr, worker_fn, arg);
pthread_join(t, nullptr); // непортируемо на Windows// not portable to Windows
std::thread t(worker_fn, arg);
t.join(); // портируемо между ОС// portable across OSes
До C++11 многопоточный код в стандарте не существовал вовсе:Before C++11, multithreaded code did not exist in the standard at all:
pthread на POSIX-системах и WinAPI-потоки на Windows — несовместимые интерфейсы; код, использующий потоки напрямую, требовал условной компиляции или обёрточных библиотек (Boost.Thread) для переносимости.pthread on POSIX systems and WinAPI threads on Windows are incompatible interfaces; code that used threads directly needed conditional compilation or wrapper libraries (Boost.Thread) to be portable.
Стандарт C++ до 2011 года формально не признавал существование потоков — компилятор мог применять оптимизации, некорректные в присутствии параллельного исполнения, потому что «многопоточности не существовало» с точки зрения стандарта. C++11 вводит формальную memory model (happens-before, data race как undefined behavior) одновременно с std::thread.The C++ standard before 2011 didn't formally acknowledge the existence of threads — the compiler could apply optimizations that were incorrect in the presence of concurrent execution, because "concurrency didn't exist" as far as the standard was concerned. C++11 introduces a formal memory model (happens-before, data races as undefined behavior) alongside std::thread.
std::thread — объект с деструктором, что позволяет интегрировать управление потоком в обычные C++ идиомы (хотя в C++11 не полностью — забытый join()/detach() вызывает std::terminate, эту проблему полноценно решит только std::jthread в C++20).std::thread is an object with a destructor, which lets thread management fit into ordinary C++ idioms (though not fully in C++11 — a forgotten join()/detach() triggers std::terminate; that problem is only fully solved by std::jthread in C++20).
std::thread — библиотечная обёртка, не отдельная модель исполнения: конструктор упаковывает переданный функтор и аргументы, затем вызывает pthread_create() (на Linux через glibc/NPTL) или соответствующий системный вызов на других платформах. Подробный путь от std::thread::thread() до живого потока ядра Linux (через clone() с CLONE_VM), а также разбор memory model, memory_order и SPSC-паттернов — в cpp_concurrency.md, здесь только происхождение фичи и базовый пример.std::thread is a library wrapper, not a separate execution model: the constructor packs up the passed functor and arguments, then calls pthread_create() (on Linux, through glibc/NPTL) or the equivalent system call on other platforms. The detailed path from std::thread::thread() to a live Linux kernel thread (via clone() with CLONE_VM), plus a breakdown of the memory model, memory_order, and SPSC patterns, lives in cpp_concurrency.md — this page only covers the feature's origin and a basic example.
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "worker " << id << "\n";
}
int main() {
std::thread t(worker, 42); // поток стартует немедленно// the thread starts immediately
// ... параллельная работа в main ...// ... concurrent work in main ...
t.join(); // дождаться завершения — обязательно// wait for completion — mandatory
// до уничтожения объекта t// before the object t is destroyed
}
Собирается ровно из трёх примитивов: фиксированный набор std::thread, общая очередь задач под mutex+condition_variable (см. bounded queue в mutex-condvar.html) и atomic-флаг остановки.Built from exactly three primitives: a fixed set of std::thread workers, a shared task queue guarded by mutex+condition_variable (see the bounded queue in mutex-condvar.html), and an atomic stop flag.
thread_pool.hpp
class ThreadPool {
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex m_;
std::condition_variable cv_;
bool stop_ = false;
public:
explicit ThreadPool(size_t n) {
for (size_t i = 0; i < n; ++i) {
workers_.emplace_back([this] { worker_loop(); });
}
}
void worker_loop() {
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lk(m_);
cv_.wait(lk, [this] { return stop_ || !tasks_.empty(); });
if (stop_ && tasks_.empty()) return; // выход только когда всё разобрано// exit only once everything is drained
task = std::move(tasks_.front());
tasks_.pop();
}
task(); // вызов вне лока — задачи не блокируют друг друга// called outside the lock — tasks don't block each other
}
}
template <class F>
void enqueue(F&& f) {
{
std::lock_guard<std::mutex> lk(m_);
tasks_.emplace(std::forward<F>(f));
}
cv_.notify_one();
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lk(m_);
stop_ = true;
}
cv_.notify_all(); // разбудить всех — проверят stop_ && пустая очередь// wake everyone up — they'll check stop_ && empty queue
for (auto& t : workers_) t.join();
}
};
Частые дополнительные вопросы: почему notify_all() в деструкторе, а не notify_one() (нужно разбудить все воркеры, ждущие на cv_, а не одного — иначе часть потоков навсегда останется спать на wait) и что вернёт задача, если нужен результат — ответ обычно ведёт к std::packaged_task/std::future вместо голого std::function<void()>, то есть к future-async.html.Common follow-up questions: why notify_all() in the destructor rather than notify_one() (you need to wake up all workers waiting on cv_, not just one — otherwise some threads stay asleep on wait forever), and what a task returns if a result is needed — the answer usually leads to std::packaged_task/std::future instead of a bare std::function<void()>, i.e. to future-async.html.
Если объект std::thread уничтожается, пока joinable() == true (ни join, ни detach не вызваны), деструктор вызывает std::terminate() — аварийное завершение всей программы, а не исключение, которое можно поймать. Путь выхода из функции через исключение — частая причина пропущенного join на практике.If a std::thread object is destroyed while joinable() == true (neither join nor detach was called), the destructor calls std::terminate() — an abrupt termination of the whole program, not an exception you could catch. An exception unwinding out of a function is a common real-world cause of a missed join.
Отсоединённый поток продолжает исполняться после уничтожения объекта std::thread и после завершения main, если явно не синхронизирован иначе — обращение к уже разрушенным объектам из detached-потока (например, захваченным по ссылке в лямбде) — классический источник use-after-free.A detached thread keeps running after the std::thread object is destroyed, and even past the end of main unless explicitly synchronized otherwise — accessing already-destroyed objects from a detached thread (e.g. ones captured by reference in a lambda) is a classic source of use-after-free.
std::thread t(f, arg) копирует или перемещает arg во внутреннее хранилище потока — передать по ссылке нужно явно через std::ref(arg), иначе функция получит копию, а не ссылку на исходный объект, что часто удивляет при первом использовании.std::thread t(f, arg) copies or moves arg into the thread's internal storage — passing by reference requires an explicit std::ref(arg), otherwise the function receives a copy rather than a reference to the original object, which often surprises people the first time around.