Última atualização:2026-10-12
C++ std::thread e jthread¶
Análise completa de thread e jthread¶
1.Preparação¶
O conteúdo deste artigo se baseia no código-fonte do gcc-13: explica em detalhes o uso e a implementação interna do std::thread do C++11, como misturar pthread + thread, e o uso e a implementação interna do jthread do C++20.
Seja você um iniciante ou um mestre em desenvolvimento multi-thread, este artigo é bastante adequado para estudar: inclui tanto a teoria básica quanto muita prática e revelação do código interno.
Versão analisada do código-fonte: gcc-13
O outline é o seguinte:
2.thread do C++11¶
2.1 Uso básico¶
Cria-se um thread e passa-se a própria função de execução, os parâmetros etc.
Aqui o result pode usar atomic ou promise.
No uso básico, geralmente é preciso atenção ao t.join: ele faz a thread principal esperar a thread filha, bloqueando a thread atual até que a thread que chamou join() termine sua execução.
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;
}
Exemplo: remova o t.join() acima e seu programa vai travar.
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 Implementação na STL¶
Vocês podem abrir o arquivo std_thread.h e ler a implementação interna junto comigo.
Caminho do código: gcc/libstdc++-v3/include/bits/std_thread.h
2.2.1 Relação com pthread¶
Primeiro, é uma classe thread, e há um tipo bastante importante: no meu sistema Linux essa macro está habilitada e, por padrão, costuma ser __gthread_t, que normalmente é um pthread_t.
#ifdef _GLIBCXX_HAS_GTHREADS
using native_handle_type = __gthread_t;
#else
using native_handle_type = int;
#endif
2.2.2 id interno¶
A classe thread tem um membro privado chamado _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 ou não¶
Se uma thread está no estado "joinable", então, antes de sua execução terminar, o programa pode chamar a função join() para esperar a conclusão da execução dessa thread.
O critério de joinable é:
Com base no conteúdo anterior, podemos deduzir: Ou seja: o critério de joinable é se a thread atual é diferente do id de thread padrão (0).O método join é muito importante e costumamos usá-lo; seu princípio de implementação, resumidamente, é o seguinte:
- Se é joinable
- Sim: chama pthread_join (o gthread aqui, na prática, chama a interface do pthread)
- Não, ou se o join acima retornou erro: lança a exceção EINVAL ou o erro do join.
- Define como não joinable, ou seja: define o id interno da thread como 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 Detach¶
Semelhante ao join, mas ao contrário: diz ao programa que você não se importa mais com a execução dessa thread. A lógica de implementação, exceto pela interface chamada, é idêntica em tudo, então não vou detalhar.
No detach é usado 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 Construção e destruição¶
2.2.5.1 Construção¶
Uso:
Código interno do construtor padrão: Uso do construtor com parâmetros: Código interno do construtor com parâmetros: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; o código aqui simplificado fica:
const int err = __gthread_create(&_M_id._M_thread,
&execute_native_thread_routine,
state.get());
if (err)
__throw_system_error(err);
__gthread_create pode ser entendido como pthread_create — ficou claro agora?
Note um detalhe aqui: o func passado como parâmetro a pthread_create é execute_native_thread_routine; a tradução acima para pthread_create fica:
E execute_native_thread_routine é uma função envolta em extern "C", para dar ligação C ao nome da função em C++.Assim, a lógica do construtor acima fica bem clara: chamar pthread_create para inicializar o id interno da thread. Mas fica uma dúvida: o que acontece com a função e os parâmetros passados pelo usuário?
Aqui entra o State: quem é atento nota que o último parâmetro de pthread_create acima é o parâmetro usual de função: state.get(), e a função é 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 é uma interface, cuja implementação é _State_impl; vê-se que os parâmetros e a função são encaminhados para cá e, por meio de _M_run, executa-se a função passada pelo usuário — na execução, é chamado o operator() de _Callable. Antes vimos que, na criação, vem com _Wrapper, então _Callable é igual a _Wrapper, e __Wrapper, por sua vez, é um alias de _Call_wrapper, que leva para dentro a função e os parâmetros do usuário.
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.
Então, suponha que eu escreva esta linha:
Ela vai chamar a função add para você por meio de invoke. Claro, o que é chamado acima é o__invoke interno; não há diferença entre os dois — um é a interface externa, o outro a interna.
Com a explicação acima, podemos resumir:
- O construtor com parâmetros cria um tid, via pthread_create
- A função passada para pthread_create é uma função envolvente, dirigida internamente pelo State
- O usuário passa função e parâmetros para o thread e, no final, a chamada dessa função é acionada por
std::__invoke.
2.2.5.2 Cópia e movimento¶
O thread proíbe o construtor de cópia e a atribuição por cópia; dá suporte apenas ao construtor de movimento e à atribuição por movimento.
Construtor de movimentothread(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 Destruição¶
O princípio da destruição é que os recursos das threads que você criou devem ser consumidos/liberados; e se uma thread é joinable, significa que ela passou por pthread_create, mas ainda não foi usada — nesse caso, um terminate é disparado diretamente.
2.2.6 Outros membros¶
Por exemplo: obter o id de thread interno do thread, e o _M_thread interno do id do thread.
id
get_id() const noexcept
{ return _M_id; }
native_handle_type
native_handle()
{ return _M_id._M_thread; }
2.2.7 Pontos de entrevista¶
1.Construção e destruição
Então, aqui vai uma pergunta: há algum problema no código abaixo?
// test1.cc
int main() {
thread t1;
}
// test2.cc
int main() {
std::thread t2(threadFunction, a, b);
}
O motivo é: o construtor padrão de thread não faz nada, enquanto o construtor com parâmetros chama __gthread_create (ou seja: pthread_create) para criar a thread, inicializando o _M_id._M_thread interno; na destruição, é verificado o joinable — nesse momento o _M_id é diferente de id(), ou seja: joinable = true, e std::__terminate() é chamado diretamente, causando o crash.
2.Atribuição por movimento
Suponha as duas funções f1 e f2 abaixo: elas podem rodar normalmente? Por quê?
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();
}
O motivo é: em f1, t2 usa o construtor padrão (que não faz nada), então o próprio t2 não é joinable e pode receber a atribuição por movimento com segurança. Já o t2 de f2 é joinable (portanto o construtor inicializou um tid), e na atribuição por movimento é verificado se é joinable; se for, termina com terminate direto. Isso é bem compreensível, pois a thread do próprio t2 foi criada e ainda não foi consumida/usada quando é trocada por swap. Como corrigir esse problema?
Simples: basta adicionar um join antes:
Algumas pessoas podem achar que dois joins em t2 não teriam problema — na verdade, o segundo t2 já é o t1, então pode-se ver como:3.jthread do C++20¶
A partir do C++20 foi introduzido o std::jthread, capaz de fazer join automaticamente da thread iniciada e de emitir um sinal de interrupção.
3.1 Uso básico¶
Se usarmos o thread anterior, escrever o código abaixo gera um core dump, porque o destrutor verifica se é joinable e, nesse caso, termina com 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 Implementação na STL¶
O jthread está dentro do thread (arquivo thread), sua implementação se baseia no thread anterior; localização no código-fonte: gcc/libstdc++-v3/include/std/thread
Qual será o segredo? Vamos revelar juntos.
3.2.1 Relação com thread¶
O jthread está intimamente ligado ao thread — pode-se dizer que é implementado com base nele. O membro mais central é do tipo thread, e o id interno e o native_handle_type usam os do thread.
3.2.2 stop_token e stop_source¶
Internamente, o jthread usa std::stop_token para reforçar o controle. O std::stop_token é um mecanismo para enviar uma solicitação de parada à execução de uma thread. Ele funciona como uma solicitação: se tratada corretamente na execução da thread, permite que ela seja terminada.
Internamente, usa-se um membro std::stop_source para manter o estado.
Vamos olhar juntos a implementação da STL.1.stop_source
Primeiro, o stop_source mantém internamente um state, chamado:
E dentro dele está: Como um stop_source pode ser mantido por threads diferentes, ele pode ser compartilhado; por isso, internamente usa-se uma referência — é por isso que você pode ver_Stop_state_ref no código-fonte, responsável por gerenciar todo o ciclo de vida do state e as questões de propriedade. Essa compartilhamento é implementado pelo _Stop_state_t de baixo nível, cujos dois membros internos são muito importantes:
using value_type = uint32_t;
std::atomic<value_type> _M_owners{1};
std::atomic<value_type> _M_value{_S_ssrc_counter_inc};
_M_owners, que gerencia a propriedade; _M_value gerencia quando parar.
As três interfaces que costumamos usar são:
- stop_possible
- stop_requested
- request_stop
Na camada de baixo nível, os bits correspondentes são definidos em _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; se o usuário chama stop_requested, também se verifica se o _M_value interno tem o _S_stop_requested_bit definido.
O _S_locked_bit está relacionado à interface stop_possible: se a solicitação de parada já foi emitida, ou se ainda existe quem possa emiti-la, stop_possible retorna true.
O outro é o _S_ssrc_counter_inc, que é o valor padrão: no construtor/construtor de cópia do stop_source, esse _S_ssrc_counter_inc é somado ao value.
2.stop_token
Praticamente todo o conteúdo do _Stop_state_ref mencionado acima está no stop_token, que também mantém internamente um state, chamado:
Aqui cita-se a explicação do cppreference: a classe stop_token fornece métodos para verificar se uma solicitação de parada já foi emitida, ou pode ser emitida, para o objeto std::stop_source associado. Ela é essencialmente uma "visão" thread-safe do estado de parada relacionado.
3.stop_callback
A relação entre stop_callback e stop_token é que o stop_callback é registrado dentro do stop_token; internamente, o stop_token mantém uma estrutura de lista duplamente ligada para remover e adicionar callbacks, e a inserção de callbacks usa inserção no início.
Há dois momentos de disparo do callback:
- Disparo no request_stop
Se o callback foi registrado com antecedência no stop_token, então, após algum tempo de execução, quando o usuário faz a solicitação, o callback é disparado.
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
Se o stop_token já solicitou parada, o callback é disparado imediatamente no registro, pois a thread já terminou de executar e o retorno de chamada deve ser executado nesse momento.
thread.request_stop();
std::stop_callback callback(stopToken, []() {
std::cout << "Callback executed" << std::endl;
}); // invoke callback
_M_register_callback chama __cb->_M_run(), que por sua vez chama a função de callback.
3.2.3 Construção e destruição¶
3.2.3.1 Construção¶
- Construtor padrão
O construtor padrão inicializa um stopstate vazio e não mantém o tid interno; o comportamento é igual ao do thread.
- Construtor com parâmetrosInicializa _M_thread: ele cria um thread por meio de _S_create chamando _S_create_pmf e o atribui a _M_thread; assim, o thread interno fica construído.
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 Cópia e movimento¶
O construtor de cópia e a atribuição por cópia foram desabilitados — comportamento igual ao do thread!
O construtor de movimento é o padrão e a atribuição por movimento usa 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 Destruição¶
A destruição define o comportamento do jthread desde o início: por que não precisamos dar join manualmente? Porque, na destruição, ele verifica se é joinable; se for joinable, significa que o usuário não deu join, e o destrutor dá o join por você, verificando antes o request_stop.
3.2.3.4 Outros membros¶
Os outros membros basicamente operam sobre o thread e expõem uma interface para fora, por exemplo:
[[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 Exemplo¶
Este exemplo do cppreference é muito bom: nele, a função de trabalho do usuário recebe um stop_source como parâmetro e, internamente, é possível verificar stop_requested(); se a parada foi solicitada, podemos fazer algumas operações de liberação de recursos — algo realmente muito útil.
A condição de disparo é a thread principal ter executado request_stop.
Tópicos relacionados¶
- Guia de C++ Concurrency in Action | C++那些事
- Depuração de Deadlocks em C++ | C++那些事
- Vetorização SIMD em C++ | C++那些事
- STL Paralela do C++17 | C++那些事
- False Sharing e Linhas de Cache em C++ | C++那些事
- Depuração All-Stop multi-thread com GDB | C++那些事
- Detecção de Erros de Concorrência com Helgrind | C++那些事
- Princípio do event loop do libev | C++那些事
- Implementação interna de std::thread em C++ | C++那些事
- Introdução a multi-thread em C++ | C++那些事