C++ (std::thread e std::jthread)
C++ oferece threads na biblioteca padrão desde o C++11, via <thread>. O tipo
std::thread funciona de maneira parecida com pthread_t, mas sem a
manipulação de ponteiros genéricos (void *) e casts manuais. Os argumentos
são passados com seus tipos reais.
Criando uma thread
O construtor de std::thread recebe um callable (função, lambda ou functor) e
os argumentos que serão passados para ele. A construção do objeto já cria e
inicia a thread (não tem uma etapa separada de “start”). t.join() bloqueia até
a thread t finalizar.
#include <iostream>
#include <thread>
void greet(const std::string &name) {
std::cout << "Hello from " << name << "!\n";
}
int main() {
std::thread t(greet, "thread");
t.join();
}
Código 4-8: Criando uma thread em C++.
Retornando valores com std::ref
std::thread não possui um mecanismo de retornar valores de uma thread de forma
direta. Uma alternativa é passar uma referência para uma variável de saída:
#include <iostream>
#include <thread>
void square(int input, int &result) {
result = input * input;
}
int main() {
int answer;
std::thread t(square, 7, std::ref(answer));
t.join();
std::cout << "7² = " << answer << "\n";
}
Código 4-9: Retornando um valor via referência em C++.
Note o std::ref(answer). Se trocarmos por apenas answer, o programa
continua compilando e rodando, mas answer fica com lixo de memória na main.
O construtor de std::thread faz decay-copy (o processo de primeiro aplicar
decay ao tipo do valor e depois copiá-lo) de todos os argumentos antes de
repassar para a thread1. Sem o std::ref, a thread recebe uma
cópia de answer e escreve nessa cópia. A variável original na main nunca é
tocada. É uma proteção contra dangling references (a variável original pode
sair de escopo antes da thread ler), mas significa que quando você quer a
referência, tem que pedir explicitamente e ser responsável por garantir que o
objeto referido irá viver por tempo suficiente até a thread terminar.
O destrutor de std::thread
Em Pthreads, esquecer de chamar pthread_join numa thread joinable causa
resource leak silencioso: o programa continua rodando, mas a memória vai
sendo consumida aos poucos. C++ tomou uma decisão diferente:
#include <thread>
int main() {
std::thread t([] {});
} /* -> t sai de escopo ao final do bloco
-> destrutor é chamado
-> destrutor detecta que t ainda é joinable
-> destrutor chama std::terminate e mata o processo
*/
Código 4-10: Destruir um std::thread joinable sem join nem detach.
$ c++ main.cpp -o main -std=c++20 -pthread
$ ./main
terminate called without an active exception
Aborted
O destrutor de std::thread invoca std::terminate() e derruba o processo
inteiro. A filosofia é que um leak silencioso é pior do que um crash. Pelo
menos o crash aparece no primeiro teste.
std::jthread
O C++20 introduziu std::jthread (joining thread), cujo destrutor faz
join() automaticamente. Mesma ideia do std::unique_ptr com delete:
#include <iostream>
#include <thread>
int main() {
std::jthread t([] {
std::cout << "Hello from jthread!\n";
});
// t sai de escopo -> join() automático
}
Código 4-11: std::jthread faz join automático no destrutor.
Se t sair de escopo por qualquer motivo (return antecipado, exceção,
final do bloco), o destrutor cuida do join().
Múltiplas threads
#include <iostream>
#include <thread>
#include <vector>
#include <format>
int main() {
const int n = 10;
std::vector<std::jthread> threads;
threads.reserve(n);
for (int i = 0; i < n; i++) {
threads.emplace_back([i] {
std::cout << std::format("Hello from thread {}!\n", i);
});
}
}
Código 4-12: Criando múltiplas threads em C++.
A lambda captura i por valor ([i]). Cada thread recebe uma cópia do
valor de i no momento da criação. Se trocarmos para captura por referência
([&i]), todas as threads enxergam a mesma variável i, e o resultado
depende de quando cada thread lê i, potencialmente depois de i já ter
sido incrementado pelo loop. O compilador aceita as duas formas sem reclamar.
$ c++ main.cpp -o main -std=c++20 -pthread
$ ./main
Hello from thread 0!
Hello from thread 3!
Hello from thread 1!
Hello from thread 2!
Hello from thread 5!
Hello from thread 4!
Hello from thread 7!
Hello from thread 6!
Hello from thread 8!
Hello from thread 9!
$ ./main
Hello from thread 1!
Hello from thread 0!
Hello from thread 2!
Hello from thread 4!
Hello from thread 3!
Hello from thread 5!
Hello from thread 6!
Hello from thread 8!
Hello from thread 7!
Hello from thread 9!
Como usamos jthread, não precisamos do join() manual: o destrutor dos
objetos cuida disso quando o vector sai de escopo.
-
O construtor de
std::threadinvoca internamentestd::invoke(decay_copy(f), decay_copy(args)...). Isso explica também por que, ao passar umconst char *como argumento, a thread recebe uma cópia do ponteiro (o que geralmente é o comportamento desejado). ↩