Последнее обновление:2026-10-12
C++ std::thread и jthread¶
Полный разбор thread и jthread¶
1. Подготовка¶
Материал этой статьи строится на исходном коде gcc-13: подробно разбирается использование C++11 std::thread и его внутренняя реализация, смешанное использование pthread + thread, а также использование C++20 jthread и его внутренняя реализация.
Если вы новичок в многопоточном разработке или, наоборот, ас — эта статья отлично подойдёт для изучения: в ней будет и базовая теория, и обширная практика, и разбор внутреннего кода.
Версия разбираемого исходного кода: gcc-13
План следующий:
2. thread в C++11¶
2.1 Базовое использование¶
Создаём thread, передавая собственную функцию выполнения, параметры и т.д.
Здесь result может быть atomic или promise.
В базовом использовании обычно нужно помнить о t.join: он заставляет главный поток дождаться дочернего — блокирует текущий поток, пока поток, для которого вызван join(), не завершит выполнение.
std::atomic<int> result(0);
void threadFunction(int a, int b) {
result = a + b;
std::cout << "Child thread finished its work.\n";
}
int main() {
int a = 5;
int b = 7;
std::thread t(threadFunction, a, b);
t.join();
std::cout << "Result computed by the child thread: " << result << std::endl;
return 0;
}
Пример: уберите t.join() выше — и ваша программа упадёт.
Result computed by the child thread: 0
Child thread finished its work.
terminate called without an active exception
[1] 52814 abort ./a.out
2.2 Реализация в STL¶
Можете открыть файл std_thread.h и читать внутреннюю реализацию вместе со мной.
Путь к коду: gcc/libstdc++-v3/include/bits/std_thread.h
2.2.1 Связь с pthread¶
Прежде всего это класс thread, в котором есть один довольно важный тип: в моей Linux-системе этот макрос включён, и по умолчанию это обычно __gthread_t, который, как правило, является pthread_t.
#ifdef _GLIBCXX_HAS_GTHREADS
using native_handle_type = __gthread_t;
#else
using native_handle_type = int;
#endif
2.2.2 Внутренний id¶
У класса thread есть приватный член _M_id.
class id
{
native_handle_type _M_thread;
public:
id() noexcept : _M_thread() { }
explicit
id(native_handle_type __id) : _M_thread(__id) { }
}
2.2.3 Присоединяемость¶
Если поток находится в состоянии «joinable», программа может до завершения его выполнения вызвать функцию join(), чтобы дождаться его окончания.
Критерий joinable:
На основе предыдущего можно вывести: То есть: критерий joinable — текущий id потока не равен id потока по умолчанию (0).Метод join очень важен, и мы обычно его используем; его принцип реализации в двух словах таков:
- joinable ли поток
- да — вызываем pthread_join (gthread здесь фактически вызовет интерфейс pthread)
- нет или выше join вернул ошибку — выбрасываем исключение EINVAL либо ошибку join.
- помечаем как не-joinable, то есть: устанавливаем внутренний id потока в 0
thread::join()
{
int __e = EINVAL;
if (_M_id != id()) // 是否可joinable
__e = __gthread_join(_M_id._M_thread, 0);
if (__e)
__throw_system_error(__e);
_M_id = id(); // _M_id = pthread_t(),设置为0
}
2.2.4 Отделение¶
Похоже на join, но наоборот: сообщаем программе, что нам больше не важно выполнение этого потока; логика реализации везде, кроме вызываемого интерфейса, одинакова — не будем повторяться.
В detach используется pthread_detach.
void
thread::detach()
{
int __e = EINVAL;
if (_M_id != id())
__e = __gthread_detach(_M_id._M_thread);
if (__e)
__throw_system_error(__e);
_M_id = id();
}
2.2.5 Конструкция и деструктор¶
2.2.5.1 Конструкция¶
Использование:
Код ядра для конструктора по умолчанию: Конструкция с параметрами: Код ядра для конструктора с параметрами:template <typename _Callable, typename... _Args,
typename = _Require<__not_same<_Callable>>>
explicit thread(_Callable &&__f, _Args &&...__args) {}
_M_start_thread(_State_ptr(new _State_impl<_Wrapper>(
std::forward<_Callable>(__f), std::forward<_Args>(__args)...)),
_M_thread_deps_never_run);
_M_start_thread; код внутри в упрощённом виде:
const int err = __gthread_create(&_M_id._M_thread,
&execute_native_thread_routine,
state.get());
if (err)
__throw_system_error(err);
__gthread_create можно понимать как pthread_create — теперь всё предельно ясно, не так ли?
Заметьте одну деталь: в параметрах, передаваемых pthread_create, функция — это execute_native_thread_routine; приведённая выше запись в терминах pthread_create:
А execute_native_thread_routine — это функция, обёрнутая в extern "C", придающая имени функции в C++ C-линковку.Так что логика конструктора выше совершенно ясна: вызывается pthread_create, инициализирующий внутренний id потока. Но возникает вопрос: что происходит с функцией и параметрами, переданными пользователем?
Здесь и появляется State: внимательные читатели заметили, что последний параметр у pthread_create выше — это обычный параметр функции: state.get(), а функцией является execute_native_thread_routine.
static void*
execute_native_thread_routine(void* __p)
{
thread::_State_ptr __t{ static_cast<thread::_State*>(__p) };
__t->_M_run();
return nullptr;
}
_State — это интерфейс, его реализация — _State_impl: видно, что параметры и функция переадресуются сюда, а затем через _M_run выполняется функция, переданная пользователем; при выполнении вызывается operator() у _Callable; ранее мы видели, что при создании передавался _Wrapper, поэтому _Callable равен _Wrapper, а __Wrapper — это псевдоним _Call_wrapper, в который передаются функция и параметры пользователя.
using _Wrapper = _Call_wrapper<_Callable, _Args...>;
struct _State
{
virtual ~_State();
virtual void _M_run() = 0;
};
using _State_ptr = unique_ptr<_State>;
template <typename _Callable>
struct _State_impl : public _State
{
_Callable _M_func;
template <typename... _Args>
_State_impl(_Args &&...__args)
: _M_func(std::forward<_Args>(__args)...)
{
}
void
_M_run() { _M_func(); }
};
std::__invoke.
Поэтому, предположим, я написал такую строку:
Она через invoke вызовет за вас функцию add. Конечно, выше вызывается внутренний__invoke — между ними нет разницы: один внешний интерфейс, другой внутренний.
Подведём итог сказанному:
- конструктор с параметрами создаёт tid через pthread_create
- функцией, передаваемой в pthread_create, является обёрнутая функция, внутри управляемая через State
- пользователь передаёт функцию и параметры в thread, а итоговый вызов этой функции приводится в действие через
std::__invoke.
2.2.5.2 Копирование и перемещение¶
thead запрещает копирующий конструктор и копирующее присваивание, поддерживая только перемещающий конструктор и перемещающее присваивание.
Перемещающий конструкторthread(thread &&__t) noexcept
{
swap(__t);
}
void
swap(thread &__t) noexcept
{
std::swap(_M_id, __t._M_id);
}
thread &operator=(thread &&__t) noexcept
{
if (joinable())
std::__terminate();
swap(__t);
return *this;
}
2.2.5.3 Деструктор¶
Принцип деструктора таков: ресурсы созданного вами потока должны быть потреблены/освобождены; если поток joinable, значит он был pthread_create-нут, но ещё не использован — в этот момент произойдёт прямой terminate.
2.2.6 Остальные члены¶
Например: получение внутреннего id потока, внутреннего _M_thread у id потока.
id
get_id() const noexcept
{ return _M_id; }
native_handle_type
native_handle()
{ return _M_id._M_thread; }
2.2.7 Темы собеседований¶
1. Конструкция и деструктор
Итак, вопрос: есть ли проблемы в приведённом ниже коде?
// test1.cc
int main() {
thread t1;
}
// test2.cc
int main() {
std::thread t2(threadFunction, a, b);
}
Причина: конструктор thread по умолчанию ничего не делает, а конструктор с параметрами вызывает __gthread_create (то есть pthread_create), создавая поток, то есть инициализирует внутренний _M_id._M_thread; в деструкторе проверяется joinable — в этот момент _M_id не равен id(), то есть joinable = true, и происходит прямой std::__terminate() — программа падает.
2. Перемещающее присваивание
Пусть есть две функции f1 и f2 — будут ли они работать нормально? Почему?
void f1() {
std::thread t1(tfunc);
std::thread t2;
t2 = std::move(t1);
t2.join();
}
void f2() {
std::thread t1(tfunc);
std::thread t2(tfunc);
t2 = std::move(t1);
t2.join();
}
Причина: в f1 у t2 использован конструктор по умолчанию (ничего не делает), поэтому сам t2 не joinable, и перемещающее присваивание безопасно. Но у t2 в f2 joinable (конструктор инициализировал tid), а при перемещающем присваивании проверяется joinable: если joinable — прямой terminate. Это довольно понятно: собственный поток t2 уже создан, ещё не потреблён, а его уже swap-нули. Как исправить эту проблему?
Очень просто: добавьте join перед этим:
Возможно, кто-то подумает: неужели два join у t2 не вызовут проблем? Но в этот момент второй t2 фактически уже является t1, так что это можно рассматривать как:3. jthread в C++20¶
Начиная с C++20 введён std::jthread, который автоматически делает join запущенного потока и отправляет сигнал прерывания.
3.1 Базовое использование¶
Если использовать предыдущий thread, следующий код упадёт: в деструкторе проверяется joinable, и в этот момент произойдёт terminate.
std::thread t{ [] {std::cout << "hello std::thread" << std::endl; } };
std::cout << "t.joinable(): " << t.joinable() << std::endl;
std::jthread tj{ [] {std::cout << "hello std::jthread" << std::endl; } };
std::cout << "tj.joinable(): " << tj.joinable() << std::endl;
3.2 Реализация в STL¶
jthread находится внутри thread, его реализация построена на предыдущем thread; расположение исходного кода: gcc/libstdc++-v3/include/std/thread
Какие же там секреты? Давайте раскроем их вместе.
3.2.1 Связь с thread¶
jthread тесно связан с thread — можно сказать, реализован на его основе; самый главный член — это тип thread, а внутренние id и native_handle_type взяты из внутренних у thread.
3.2.2 stop_token и stop_source¶
Внутри jthread для усиления управления используется std::stop_token. std::stop_token — это механизм отправки запроса на остановку выполнения потока. Он работает как запрос: если он правильно обрабатывается при выполнении потока, это позволяет его завершить.
Внутри для поддержания используется член std::stop_source.
Давайте вместе посмотрим на реализацию STL.1.stop_source
Прежде всего stop_source внутри поддерживает state, называемый:
А в нём: Поскольку один stop_source может удерживаться разными потоками, он может быть разделяемым, поэтому внутри всё сделано через ссылку — потому в исходном коде вы можете увидеть_Stop_state_ref, который управляет всем жизненным циклом state и вопросами владения. Совместное использование реализовано через лежащий в основе _Stop_state_t; внутри есть два очень важных члена:
using value_type = uint32_t;
std::atomic<value_type> _M_owners{1};
std::atomic<value_type> _M_value{_S_ssrc_counter_inc};
_M_owners, управляющий владением; _M_value отвечает за то, когда остановиться.
Три интерфейса, которыми мы обычно пользуемся:
- stop_possible
- stop_requested
- request_stop
Внизу просто устанавливаются соответствующие биты в _M_value
static constexpr value_type _S_stop_requested_bit = 1;
static constexpr value_type _S_locked_bit = 2;
static constexpr value_type _S_ssrc_counter_inc = 4;
_S_stop_requested_bit; если пользователь вызвал stop_requested, то так же проверяется, установлен ли внутри _M_value бит _S_stop_requested_bit.
_S_locked_bit связан с интерфейсом stop_possible: если запрос на остановку уже отправлен или ещё существует возможность его отправить, stop_possible вернёт true.
Ещё один — _S_ssrc_counter_inc; это значение по умолчанию: при конструировании/копирующем конструировании stop_source к value прибавляется _S_ssrc_counter_inc.
2.stop_token
Всё, что говорилось выше про _Stop_state_ref, практически целиком находится в stop_token; внутри он также поддерживает state, называемый:
Здесь процитирую объяснение с cppreference: класс stop_token предоставляет методы проверки того, был ли или может ли быть отправлен запрос на остановку для связанного с ним объекта std::stop_source. По сути, это потокобезопасное «представление» соответствующего состояния остановки.
3.stop_callback
Отношение stop_callback и stop_token таково: stop_callback регистрируется в stop_token, а внутри stop_token для удаления и добавления callback поддерживается двусвязный список; вставка callback выполняется в голову списка.
Моментов срабатывания callback два:
- срабатывание при request_stop
Callback заранее регистрируется в stop_token; через некоторое время выполнения пользователь инициирует запрос — в этот момент срабатывает callback.
auto onStop = []() {
std::cout << "Timer has stopped." << std::endl;
};
// Register the onStop callback with stop_callback
std::stop_callback callback(source.get_token(), onStop);
// do something for example: sleep
// stop
source.request_stop(); // invoke callback
stop_token уже запросил остановку — тогда callback сработает сразу при регистрации: поток уже завершил выполнение, и в этот момент следует выполнить колбэк.
thread.request_stop();
std::stop_callback callback(stopToken, []() {
std::cout << "Callback executed" << std::endl;
}); // invoke callback
_M_register_callback вызовет __cb->_M_run(), который и вызывает функцию callback.
3.2.3 Конструкция и деструктор¶
3.2.3.1 Конструкция¶
- Конструктор по умолчанию
Конструктор по умолчанию инициализирует пустой stopstate, внутренний tid не поддерживается — поведение как у thread.
- Конструктор с параметрамиИнициализирует _M_thread: он через _S_create, а затем вызов _S_create_pmf создаёт thread и присваивает его _M_thread — так внутренний thread оказывается сконструирован.
template<typename _Callable, typename... _Args,
typename = enable_if_t<!is_same_v<remove_cvref_t<_Callable>,
jthread>>>
explicit
jthread(_Callable&& __f, _Args&&... __args)
: _M_thread{_S_create(_M_stop_source, std::forward<_Callable>(__f),
std::forward<_Args>(__args)...)}
{ }
3.2.3.2 Копирование и перемещение¶
Копирующий конструктор и копирующее присваивание запрещены — поведение как у thread!
Перемещающий конструктор — default, перемещающее присваивание использует swap.jthread(jthread&&) noexcept = default;
jthread&
operator=(jthread&& __other) noexcept
{
std::jthread(std::move(__other)).swap(*this);
return *this;
}
void
swap(jthread& __other) noexcept
{
std::swap(_M_stop_source, __other._M_stop_source);
std::swap(_M_thread, __other._M_thread);
}
3.2.3.3 Деструктор¶
Деструктор определяет изначальное поведение jthread: почему нам не нужно делать join вручную? Потому что в деструкторе проверяется joinable: если joinable, значит пользователь не делал join, и деструктор сам вызывает за вас join, предварительно проверив request_stop.
3.2.3.4 Остальные члены¶
Остальные члены в основном работают поверх thread и лишь выставляют наружу интерфейс, например:
[[nodiscard]] bool
joinable() const noexcept
{
return _M_thread.joinable();
}
join()
{
_M_thread.join();
}
void
detach()
{
_M_thread.detach();
}
[[nodiscard]] id
get_id() const noexcept
{
return _M_thread.get_id();
}
[[nodiscard]] native_handle_type
native_handle()
{
return _M_thread.native_handle();
}
[[nodiscard]] static unsigned
hardware_concurrency() noexcept
{
return thread::hardware_concurrency();
}
[[nodiscard]] stop_source
get_stop_source() noexcept
{
return _M_stop_source;
}
[[nodiscard]] stop_token
get_stop_token() const noexcept
{
return _M_stop_source.get_token();
}
bool request_stop() noexcept
{
return _M_stop_source.request_stop();
}
3.3 Пример¶
Этот пример из cppreference очень хорош: в нём в рабочую функцию пользователя передаётся stop_source в качестве параметра; внутри можно проверять stop_requested(), и если остановка произошла, выполнить освобождение ресурсов — очень полезно.
Условие срабатывания — главный поток выполнил request_stop.
Похожие темы¶
- Путеводитель по C++ Concurrency in Action | C++那些事
- Поиск взаимоблокировок в C++ | C++那些事
- Векторизация C++ SIMD | C++那些事
- Параллельный STL в C++17 | C++那些事
- C++ False Sharing и строка кэша | C++那些事
- Отладка многопоточности GDB в режиме All-Stop | C++那些事
- Обнаружение ошибок конкурентности с помощью Helgrind | C++那些事
- Принцип событийного цикла libev | C++那些事
- Внутренняя реализация C++ std::thread | C++那些事
- Введение в многопоточность C++ | C++那些事