Ir para o conteúdo

Ú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

https://github.com/gcc-mirror/gcc/tree/releases/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;
}
Se você não especificar join() ou detach() na thread, ocorrerá um erro em tempo de execução, porque a thread principal/atual concluirá sua execução enquanto as outras threads criadas ainda estarão em execução.

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
Ou seja:

typedef pthread_t __gthread_t;
Portanto, chegamos à primeira conclusão: a implementação de baixo nível é baseada no pthread do Linux.

2.2.2 id interno

A classe thread tem um membro privado chamado _M_id.

private:
    id              _M_id;
Seu tipo é uma classe interna e, ao aprofundar, chega-se ao native_handle_type.

class id
{
  native_handle_type    _M_thread;

public:
  id() noexcept : _M_thread() { }

  explicit
  id(native_handle_type __id) : _M_thread(__id) { }

}
Combinando com o conteúdo acima, podemos obter:

private:
    pthread_t               _M_id;
Familiar, não? Temos o segundo ponto de conhecimento: thread mantém internamente um id que, no Linux, é por padrão um pthread_t.

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 é:

_M_id != id()
Com base no conteúdo anterior, podemos deduzir:

pthread_t _M_id;
pthread_t null_thread = pthread_t();
_M_id != null_thread;
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
}
Depois do join, se dermos join novamente, será lançada a exceção EINVAL mencionada acima, isto é: argumento inválido.

terminate called after throwing an instance of 'std::system_error'
  what():  Invalid argument

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:

thread t1;
Código interno do construtor padrão:

thread() noexcept = default;
Uso do construtor com parâmetros:

std::thread t2(threadFunction, a, b);
Código interno do construtor com parâmetros:

template <typename _Callable, typename... _Args,
          typename = _Require<__not_same<_Callable>>>
explicit thread(_Callable &&__f, _Args &&...__args) {}
A implementação interna é:

_M_start_thread(_State_ptr(new _State_impl<_Wrapper>(
                std::forward<_Callable>(__f), std::forward<_Args>(__args)...)),
            _M_thread_deps_never_run);
Vê-se que a função central é _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);
E __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:

pthread_create (&_M_id._M_thread, NULL, &execute_native_thread_routine, state.get());
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(); }
};
Então, chegando aqui, podemos olhar

template <typename... _Tp>
using _Call_wrapper = _Invoker<tuple<typename decay<_Tp>::type...>>;
Isso equivale a tirar as referências e os modificadores de todos os parâmetros do _Call_wrapper acima e passá-los como tuple, ou seja:

_Invoker<tuple<_Callable, Arg1, Arg2, ...>
E, por fim, é passado para std::__invoke.

Então, suponha que eu escreva esta linha:

_Call_wrapper<decltype(&add), int, int> invoker(&add, 3, 4);
Ela vai chamar a função add para você por meio de invoke.

invoke(_Fn&& __f, _Args&&... __args)
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.

thread(const thread &) = delete;
thread &operator=(const thread &) = delete;
Construtor de movimento

thread(thread &&__t) noexcept
{
  swap(__t);
}

void
swap(thread &__t) noexcept
{
    std::swap(_M_id, __t._M_id);
}
Atribuição por movimento

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.

~thread()
{
  if (joinable())
    std::__terminate();
}

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);
}
A resposta é: test1 roda normalmente, test2 dá crash!

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();
}
A resposta é: f1 roda normalmente, f2 dá crash!

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:

t2.join();  // 新加的
t2 = std::move(t1);
t2.join();
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:

t2.join();
t1.join();

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;
Com o jthread do C++20, esse problema deixa de existir, por exemplo:

std::jthread tj{ [] {std::cout << "hello std::jthread" << std::endl; } };

std::cout << "tj.joinable(): " << tj.joinable() << std::endl;
Você verá que agora roda normalmente!

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.

using id = thread::id;
using native_handle_type = thread::native_handle_type;

thread _M_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.

stop_source _M_stop_source;
Vamos olhar juntos a implementação da STL.

1.stop_source

Primeiro, o stop_source mantém internamente um state, chamado:

stop_token::_Stop_state_ref _M_state;
E dentro dele está:

_Stop_state_t* _M_ptr
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};
Um é _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;
Quando o usuário pede a parada com request_stop, é definido o _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:

_Stop_state_ref _M_state;
Os detalhes de implementação são iguais aos acima e não serão repetidos. Mas qual é a diferença entre ele e o stop_source?

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.

https://en.cppreference.com/w/cpp/thread/stop_token

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
- Disparo imediato no registro

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
A implementação concreta na STL é que _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.

jthread() noexcept
  : _M_stop_source{nostopstate}
  { }
- Construtor com parâmetros

Inicializa _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!

jthread(const jthread&) = delete;
jthread& operator=(const jthread&) = delete;
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.

~jthread()
{
  if (joinable())
    {
      request_stop();
      join();
    }
}
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();
}
Restam algumas operações sobre o stop_source:

[[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.

https://en.cppreference.com/w/cpp/thread/stop_source

Tópicos relacionados

Comentários