C++ Threads
Até aqui, todo programa fez uma coisa de cada vez: uma linha, depois a próxima. Mas o computador da Ana tem vários núcleos de processador, e muitos programas fazem várias coisas ao mesmo tempo: um jogo desenha a tela enquanto toca a música; um navegador baixa várias imagens de uma vez. Cada uma dessas "linhas de trabalho" é uma thread. Nesta aula você vai criar threads, ver os erros clássicos acontecerem de verdade (inclusive um que muda de resultado a cada execução) e aprender a primeira ferramenta para evitá-los, o std::mutex.
- O que é concorrência e o que é uma thread.
- Criar uma thread com
std::threade esperar por ela comjoin(). - O que acontece quando você esquece o
join()(terminate, o erro real). - Passar argumentos para a thread, e por que referências precisam de
std::ref. - O que é uma condição de corrida, com um contador que dá um resultado diferente a cada execução.
- Proteger dados com
std::mutexestd::lock_guard. std::jthreadC++20, que espera sozinho, e ostd::async, só de passagem.
O editor "Experimente" roda o seu programa dentro do navegador, num ambiente (WebAssembly) que não oferece threads. Um programa com std::thread até compila no editor, mas, ao rodar, termina assim (é a mensagem real do editor, que usa a biblioteca libc++ do Clang):
libc++abi: terminating due to uncaught exception of type std::__2::system_error: thread constructor failed: Not supported
("O construtor da thread falhou: não suportado".) Por isso, os programas com std::thread desta aula aparecem só no terminal do laboratório, com as saídas reais. As caixas de "Experimente" ensinam as partes que funcionam sem threads: a lógica de uma condição de corrida, simulada passo a passo, e a divisão de um trabalho em partes. Para rodar as threads de verdade, use o g++ no seu computador (aula Instalar no Linux).
Os programas foram compilados com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp, com o comando padrão g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Todos usam no máximo duas threads além da principal e terminam em menos de um segundo. Você vai precisar de Funções, Por Valor × Por Referência e Lambdas.
A ideia: duas pessoas na mesma cozinha
Hoje é dia de almoço especial na casa da Ana. Se ela fizer tudo sozinha, primeiro cozinha, depois lava a louça. Se o Bruno ajudar, os dois trabalham ao mesmo tempo: ela cozinha enquanto ele lava. Isso é concorrência: vários trabalhos andando juntos.
Mas trabalhar junto traz problemas novos:
- A Ana precisa esperar o Bruno terminar antes de servir o almoço (isso vai ser o
join). - Se os dois forem marcar no mesmo caderninho quantos pratos já ficaram prontos, um pode apagar a marca do outro (isso vai ser a condição de corrida).
- A solução para o caderninho: uma regra de "um de cada vez", como uma chave que só um pode segurar (isso vai ser o
mutex).
| Palavra | Quer dizer |
|---|---|
| Thread ("fio", "linha") | Uma linha de execução: uma sequência de instruções rodando. Todo programa começa com uma, a do main. |
| Concorrência | Vários trabalhos em andamento ao mesmo tempo. |
| Paralelismo | Vários trabalhos rodando literalmente no mesmo instante, em núcleos diferentes do processador. |
1. A primeira thread: std::thread e join
#include <iostream>
#include <thread>
void lavar_louca() {
std::cout << "Bruno: lavando a louça\n";
}
int main() {
std::thread ajudante(lavar_louca);
std::cout << "Ana: fazendo o almoço\n";
ajudante.join();
std::cout << "Ana: tudo pronto!\n";
}
$ g++ -std=c++23 -Wall -Wextra primeira.cpp -o primeira $ ./primeira Ana: fazendo o almoço Bruno: lavando a louça Ana: tudo pronto!
- #include <thread>
- O cabeçalho das threads.
- std::thread ajudante(lavar_louca);
- Cria uma thread chamada
ajudante, que começa imediatamente a executar a funçãolavar_louca. Repare: o nome da função vai sem parênteses. Você não está chamando a função; está entregando-a para a thread chamar. - std::cout << "Ana: …"
- Enquanto isso, o
maincontinua na thread principal. - ajudante.join();
- Join = "juntar-se". O
mainpara e espera até a threadajudanteterminar. Depois dojoin, é garantido que a louça foi lavada.
Quem escreveu primeiro, a Ana ou o Bruno? Não dá para saber de antemão. Aqui a Ana venceu, porque criar uma thread leva um tempinho; em outro computador, ou num momento em que o sistema estiver ocupado, a ordem pode se inverter. Em programas com threads, a ordem entre threads diferentes não é garantida, a menos que você mesmo a garanta (com join, por exemplo). O "tudo pronto!", esse sim, sempre sai por último: ele vem depois do join.
-pthread?
Em tutoriais antigos, o comando para compilar threads no Linux tem -pthread no fim. Conferimos: com o g++ 15 do Ubuntu 26.04, o comando padrão do guia funciona sem ela, porque a biblioteca de threads passou a fazer parte da biblioteca C principal (glibc 2.34, de 2021). Em sistemas mais antigos, sem -pthread o programa pode falhar ao compilar ou ao rodar. Acrescentá-la não faz mal: se for usar um Linux antigo, escreva g++ -std=c++23 -Wall -Wextra -pthread ….
Esquecer o join: terminate
Toda std::thread criada precisa de um join() antes de ser destruída (ou de um detach(), que a "solta" para sempre e quase nunca é uma boa ideia). Se o main chegar ao fim com a thread ainda "presa" ao objeto, o C++ derruba o programa inteiro:
#include <iostream>
#include <thread>
void lavar_louca() {
std::cout << "Bruno: lavando a louça\n";
}
int main() {
std::thread ajudante(lavar_louca);
std::cout << "Ana: fazendo o almoço\n";
}
$ g++ -std=c++23 -Wall -Wextra sem_join.cpp -o sem_join « compila sem aviso nenhum $ ./sem_join Ana: fazendo o almoço terminate called without an active exception Abortado (imagem do núcleo gravada) ./sem_join $ echo $? 134
- terminate called without an active exception
- "
terminatefoi chamado sem nenhuma exceção ativa."std::terminateé a função que encerra o programa à força. Normalmente ela aparece por causa de uma exceção não capturada (aula Exceções); aqui, não houve exceção: foi o objetoajudantesendo destruído no fim domainsem ter feitojoin. - Abortado
- O programa morreu com o sinal de abortar (código de saída 134 = 128 + 6).
E o Bruno nem chegou a lavar a louça: o programa morreu antes. A regra: para cada std::thread, um join(). Ou use a std::jthread da seção 5, que faz o join sozinha.
2. Passar argumentos
Depois do nome da função, coloque os argumentos que ela recebe, separados por vírgula:
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
void baixar(std::string arquivo, int partes) {
for (int i = 1; i <= partes; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << arquivo + ": parte " + std::to_string(i) + " de " + std::to_string(partes) + "\n";
}
}
int main() {
std::thread t1(baixar, "fotos.zip", 3);
std::thread t2(baixar, "musica.mp3", 2);
t1.join();
t2.join();
std::cout << "Downloads concluídos\n";
}
$ g++ -std=c++23 -Wall -Wextra argumentos.cpp -o argumentos $ ./argumentos fotos.zip: parte 1 de 3 musica.mp3: parte 1 de 2 « os dois "downloads" andam juntos fotos.zip: parte 2 de 3 musica.mp3: parte 2 de 2 fotos.zip: parte 3 de 3 Downloads concluídos
Os dois "downloads" (fingidos, com o sleep_for da aula Tempo e Datas) andaram lado a lado: o programa todo levou cerca de 0,3 s, e não 0,5 s, como levaria fazendo um depois do outro. A ordem entre as linhas de fotos.zip e musica.mp3 pode variar de uma execução para outra. Repare também que cada linha é montada inteira com + antes de ir para o cout: com vários << separados, os pedaços das duas threads poderiam se misturar no meio da linha.
Referências precisam de std::ref
A std::thread copia todos os argumentos, por segurança: a thread pode viver mais que as variáveis de quem a criou. Se a função recebe uma referência (para devolver um resultado, por exemplo), você precisa dizer explicitamente "passe por referência" com std::ref, de <functional>:
#include <functional>
#include <iostream>
#include <thread>
void contar_ate(int limite, int& resultado) {
int soma = 0;
for (int i = 1; i <= limite; ++i) {
soma += i;
}
resultado = soma;
}
int main() {
int total = 0;
std::thread t(contar_ate, 100, std::ref(total));
t.join();
std::cout << "Soma de 1 a 100: " << total << '\n';
}
$ g++ -std=c++23 -Wall -Wextra referencia.cpp -o referencia $ ./referencia Soma de 1 a 100: 5050
Sem o std::ref (escrevendo só total), o g++ recusa:
$ g++ -std=c++23 -Wall -Wextra sem_ref.cpp -o sem_ref
In file included from /usr/include/c++/15/stop_token:40,
from /usr/include/c++/15/thread:44,
from sem_ref.cpp:3:
/usr/include/c++/15/bits/std_thread.h: In instantiation of ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(int, int&); _Args = {int, int&}; <template-parameter-1-3> = void]’:
sem_ref.cpp:15:41: required from here
15 | std::thread t(contar_ate, 100, total);
| ^
/usr/include/c++/15/bits/std_thread.h:168:72: error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues
168 | typename decay<_Args>::type...>::value,
| ^~~~~
« … e mais 19 linhas
A frase importante é std::thread arguments must be invocable after conversion to rvalues: "os argumentos da std::thread precisam funcionar depois de convertidos em valores temporários". Traduzindo: a thread copiou o total para uma cópia temporária, e uma referência int& não pode se ligar a uma cópia temporária. Isso é proposital: se deixasse, a função alteraria a cópia, e o seu total continuaria 0 sem você entender por quê. A linha sem_ref.cpp:15:41: required from here aponta a sua linha.
Com std::ref(total), a thread mexe na variável total do main. Ela precisa existir até a thread terminar, ou seja, até o join. É por isso que o join vem antes de usar o total.
Em vez de uma função separada, também dá para passar uma lambda: std::thread t([&total] { total = 42; });. A captura [&total] faz o papel do std::ref.
3. Condição de corrida: o contador que perde contas
Duas threads vão contar visitas num contador compartilhado, 100 mil cada uma. O resultado deveria ser 200 mil:
#include <iostream>
#include <thread>
int contador = 0;
void visitar() {
for (int i = 0; i < 100000; ++i) {
++contador;
}
}
int main() {
std::thread a(visitar);
std::thread b(visitar);
a.join();
b.join();
std::cout << "Esperado: 200000, obtido: " << contador << '\n';
}
$ g++ -std=c++23 -Wall -Wextra corrida.cpp -o corrida $ ./corrida Esperado: 200000, obtido: 102492 $ ./corrida Esperado: 200000, obtido: 111838 $ ./corrida Esperado: 200000, obtido: 105624 « três execuções, três resultados, todos errados
Quase metade das visitas sumiu, e cada execução perdeu uma quantidade diferente. No seu computador, os números serão outros (podem até, por sorte, sair certos de vez em quando, o que torna esse erro ainda mais traiçoeiro). Esse defeito se chama condição de corrida (race condition): o resultado depende de quem "corre" mais rápido.
Por que as contas se perdem
++contador parece uma coisa só, mas para o processador são três passos: ler o valor da memória, somar 1, escrever de volta. Se as duas threads intercalarem esses passos, uma escrita apaga a outra:
| Passo | Thread A | Thread B | contador na memória |
|---|---|---|---|
| 1 | lê: 0 | 0 | |
| 2 | lê: 0 | 0 | |
| 3 | soma: 0 + 1 = 1 | 0 | |
| 4 | soma: 0 + 1 = 1 | 0 | |
| 5 | escreve 1 | 1 | |
| 6 | escreve 1 | 1 (deveria ser 2) |
Duas visitas, uma só contada. Repetido milhares de vezes, sobra o resultado que você viu. Para o C++, duas threads mexendo na mesma variável ao mesmo tempo, com pelo menos uma escrevendo, é um data race ("corrida de dados") e é comportamento indefinido: perder contas é só um dos efeitos possíveis. O compilador não avisa nada. Para pegar esse erro ao rodar, existe o ThreadSanitizer, -fsanitize=thread (aula Sanitizers).
Você pode reproduzir a tabela sem threads, executando os passos na ordem errada de propósito. Isso funciona no editor:
Missão: o programa simula, sem threads, a tabela acima: Ana e Bruno leem o contador antes de o outro escrever, e uma soma se perde (sai Contador: 1). Reordene as linhas, sem mudar nenhuma, para que o Bruno só leia depois de a Ana escrever. A saída deve ser Contador: 2. É exatamente isso que o mutex da próxima seção garante.
#include <iostream>
int main() {
int contador = 0;
// Ana e Bruno querem somar 1 ao contador "ao mesmo tempo".
int copia_ana = contador; // 1. Ana lê o contador
int copia_bruno = contador; // 2. Bruno lê o contador
copia_ana = copia_ana + 1; // 3. Ana soma 1 na cópia dela
copia_bruno = copia_bruno + 1; // 4. Bruno soma 1 na cópia dele
contador = copia_ana; // 5. Ana escreve o resultado
contador = copia_bruno; // 6. Bruno escreve o resultado
std::cout << "Contador: " << contador << '\n';
}
#include <iostream>
int main() {
int contador = 0;
// Ana e Bruno querem somar 1 ao contador "ao mesmo tempo".
int copia_ana = contador; // 1. Ana lê o contador
copia_ana = copia_ana + 1; // 3. Ana soma 1 na cópia dela
contador = copia_ana; // 5. Ana escreve o resultado
int copia_bruno = contador; // 2. Bruno lê o contador
copia_bruno = copia_bruno + 1; // 4. Bruno soma 1 na cópia dele
contador = copia_bruno; // 6. Bruno escreve o resultado
std::cout << "Contador: " << contador << '\n';
}
4. std::mutex e std::lock_guard: um de cada vez
Um mutex (de mutual exclusion, "exclusão mútua") é como a chave do banheiro de um posto de gasolina: só quem está com a chave entra; os outros esperam na porta. Quem tranca o mutex antes de mexer no contador tem certeza de que ninguém mais está mexendo nele ao mesmo tempo.
#include <iostream>
#include <mutex>
#include <thread>
int contador = 0;
std::mutex trava;
void visitar() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> guarda(trava);
++contador;
}
}
int main() {
std::thread a(visitar);
std::thread b(visitar);
a.join();
b.join();
std::cout << "Esperado: 200000, obtido: " << contador << '\n';
}
$ g++ -std=c++23 -Wall -Wextra mutex.cpp -o mutex $ ./mutex Esperado: 200000, obtido: 200000 $ ./mutex Esperado: 200000, obtido: 200000 « certo, toda vez
- #include <mutex>
- O cabeçalho do mutex e do
lock_guard. - std::mutex trava;
- O mutex, compartilhado pelas threads (como o contador). Ele começa destrancado.
- std::lock_guard<std::mutex> guarda(trava);
- Tranca o mutex. Se outra thread estiver com ele, esta espera aqui até ele ser liberado. Lock guard = "guarda da tranca".
- ++contador;
- Agora os três passos (ler, somar, escrever) acontecem sem ninguém no meio.
- } (fim da volta do for)
- O
guardasai de escopo e destranca sozinho o mutex. É a técnica RAII (aula Destrutores e RAII).
Por que não chamar trava.lock() e trava.unlock() na mão? Funciona, mas se você esquecer o unlock (ou se uma exceção acontecer no meio), o mutex fica trancado para sempre e as outras threads esperam eternamente: o programa trava (um deadlock). O lock_guard não esquece.
Com o mutex, as duas threads passam boa parte do tempo esperando uma pela outra. No laboratório, medindo com time, a versão com mutex levou cerca de 0,026 s, contra 0,003 s da versão sem proteção (que dá o resultado errado): quase dez vezes mais. O mutex garante o resultado certo; a velocidade vem de dividir o trabalho para que as threads mexam o mínimo possível em dados compartilhados, como na missão final. Para um simples contador, também existe o std::atomic<int>, de <atomic>, que faz o ++ inteiro de uma vez só, sem mutex.
5. std::jthread: a thread que espera sozinha C++20
O C++20 trouxe a std::jthread ("joining thread", "thread que se junta"). Ela é igual à std::thread, com uma diferença: quando o objeto é destruído, ela faz o join sozinha. Lembra do programa que morria por falta de join? Trocando só o tipo:
#include <iostream>
#include <thread>
void lavar_louca() {
std::cout << "Bruno: lavando a louça\n";
}
int main() {
std::jthread ajudante(lavar_louca);
std::cout << "Ana: fazendo o almoço\n";
}
$ g++ -std=c++23 -Wall -Wextra jthread.cpp -o jthread $ ./jthread Ana: fazendo o almoço Bruno: lavando a louça « no fim do main, a jthread esperou o Bruno
Em código novo, prefira std::jthread: é o mesmo princípio do lock_guard, o objeto cuida da limpeza sozinho. Ela também sabe receber um pedido de "pare" (request_stop), útil para threads que ficam rodando num laço; isso fica para quando você precisar. Com -std=c++17, o g++ avisa: error: ‘jthread’ is not a member of ‘std’ e note: ‘std::jthread’ is only available from C++20 onwards.
std::async: só para conhecer
Muitas vezes o que você quer é "calcule isto em paralelo e me dê o resultado depois". O std::async, de <future>, faz isso sem você criar a thread na mão: ele devolve um std::future ("futuro"), uma promessa de resultado, e o .get() espera e entrega o valor:
#include <future>
#include <iostream>
long long somar(int de, int ate) {
long long s = 0;
for (int i = de; i <= ate; ++i) s += i;
return s;
}
int main() {
std::future<long long> metade1 = std::async(std::launch::async, somar, 1, 500);
long long metade2 = somar(501, 1000);
std::cout << "Total: " << metade1.get() + metade2 << '\n';
}
$ g++ -std=c++23 -Wall -Wextra assincrono.cpp -o assincrono $ ./assincrono Total: 500500
Enquanto uma thread soma de 1 a 500, o main soma de 501 a 1000. O std::launch::async pede uma thread nova de verdade. Os detalhes de future e async vão além deste guia; guarde que existem.
Dividir o trabalho
O uso mais comum de threads é este: um trabalho grande, dividido em partes independentes, cada parte numa thread, e no fim os resultados se juntam. Cada thread escreve na sua própria variável, então não há corrida e não é preciso mutex:
#include <iostream>
#include <thread>
void somar_intervalo(int inicio, int fim, long long& resultado) {
long long soma = 0;
for (int i = inicio; i <= fim; ++i) {
soma += i;
}
resultado = soma;
}
int main() {
long long parte1 = 0;
long long parte2 = 0;
std::thread t1(somar_intervalo, 1, 50, std::ref(parte1));
std::thread t2(somar_intervalo, 51, 100, std::ref(parte2));
t1.join();
t2.join();
std::cout << "Parte 1: " << parte1 << '\n';
std::cout << "Parte 2: " << parte2 << '\n';
std::cout << "Total: " << parte1 + parte2 << '\n';
}
$ g++ -std=c++23 -Wall -Wextra dividir.cpp -o dividir $ ./dividir Parte 1: 1275 Parte 2: 3775 Total: 5050
Para somar 100 números, threads não valem a pena: criar uma thread custa mais que essa conta. Elas compensam com trabalhos que levam segundos (processar milhares de fotos, por exemplo). E mais threads não é sempre melhor: além do número de núcleos do processador, elas só disputam o mesmo lugar. A missão final pratica a divisão das partes; no editor, sem threads, as duas chamadas rodam uma depois da outra, mas a lógica de "cada parte com sua variável" é a mesma.
Quando dá errado
terminate called without an active exception
O que significa: um objeto std::thread foi destruído (no fim do bloco em que foi criado) sem join() nem detach().
Como consertar: chame t.join() antes do fim do bloco, ou use std::jthread.
static assertion failed: std::thread arguments must be invocable after conversion to rvalues
O que significa: a função da thread recebe uma referência (int&), e você passou a variável direto. A thread copia os argumentos, e a cópia não serve para a referência.
Como consertar: passe std::ref(variavel) (com #include <functional>), ou use uma lambda que capture por referência.
O que significa: condição de corrida: duas threads mexem na mesma variável sem proteção. É comportamento indefinido, e o compilador não avisa.
Como consertar: proteja o acesso com std::mutex + std::lock_guard, use std::atomic para contadores simples, ou, melhor ainda, dê a cada thread sua própria variável e junte no fim. Para encontrar a corrida: -fsanitize=thread.
O que significa: provavelmente um deadlock: uma thread espera um mutex que nunca é liberado (um lock() sem unlock()), ou duas threads esperam uma pela outra.
Como consertar: aperte Ctrl+C para parar. Use sempre std::lock_guard em vez de lock/unlock na mão, e mantenha o trecho trancado o menor possível.
Referência rápida
| Escrita | O que faz |
|---|---|
std::thread t(funcao, arg1, arg2); | Cria uma thread que roda funcao(arg1, arg2) (<thread>) |
t.join(); | Espera a thread terminar |
t.detach(); | Solta a thread (evite) |
std::ref(x) | Passa x por referência para a thread (<functional>) |
std::jthread t(funcao); | Thread que faz join sozinha no fim (C++20) |
std::mutex m; | Uma tranca (<mutex>) |
std::lock_guard<std::mutex> g(m); | Tranca até o fim do bloco |
std::atomic<int> c = 0; | Contador seguro sem mutex (<atomic>) |
std::async(std::launch::async, f, args…) | Roda f em paralelo; resultado com .get() (<future>) |
std::this_thread::sleep_for(100ms); | Pausa a thread atual |
Exercícios
1. Esperar
Complete para esperar a thread t terminar:
2. Por referência
Complete para a thread receber total por referência:
3. O que acontece se o main terminar com uma std::thread sem join()?
4. Duas threads fazem ++contador 1000 vezes cada, sem mutex. O que se pode afirmar sobre o resultado?
5. Qual a vantagem do std::lock_guard sobre chamar lock() e unlock() na mão?
Missão final: dividir a soma em duas partes
Missão: a soma de 1 a 100 foi dividida em duas partes, como se cada uma fosse para uma thread. Só a primeira parte está sendo calculada. Chame somar_intervalo para a segunda parte (de 51 a 100, guardando em parte2). A saída deve terminar com Parte 2: 3775 e Total: 5050. (No laboratório, com std::thread, a versão com threads deu o mesmo resultado; veja a seção "Dividir o trabalho".)
#include <iostream>
void somar_intervalo(int inicio, int fim, long long& resultado) {
long long soma = 0;
for (int i = inicio; i <= fim; ++i) {
soma += i;
}
resultado = soma;
}
int main() {
long long parte1 = 0;
long long parte2 = 0;
somar_intervalo(1, 50, parte1);
// calcule a parte 2 aqui
std::cout << "Parte 1: " << parte1 << '\n';
std::cout << "Parte 2: " << parte2 << '\n';
std::cout << "Total: " << parte1 + parte2 << '\n';
}
#include <iostream>
void somar_intervalo(int inicio, int fim, long long& resultado) {
long long soma = 0;
for (int i = inicio; i <= fim; ++i) {
soma += i;
}
resultado = soma;
}
int main() {
long long parte1 = 0;
long long parte2 = 0;
somar_intervalo(1, 50, parte1);
somar_intervalo(51, 100, parte2);
std::cout << "Parte 1: " << parte1 << '\n';
std::cout << "Parte 2: " << parte2 << '\n';
std::cout << "Total: " << parte1 + parte2 << '\n';
}
Resumo
- Uma thread é uma linha de execução.
std::thread t(funcao, args…);cria e já começa a rodar. - Toda
std::threadprecisa dejoin(); sem ele, o programa morre comterminate called without an active exception.std::jthread(C++20) faz ojoinsozinha. - A ordem entre threads não é garantida. Só o
join(e as travas) põem ordem. - Argumentos são copiados; para referência,
std::ref(x). - Duas threads mexendo na mesma variável sem proteção = condição de corrida: resultados errados e diferentes a cada execução, sem aviso do compilador.
std::mutex+std::lock_guard: um de cada vez, destrancando sozinho no fim do bloco.- O melhor é dividir o trabalho para que cada thread tenha seus próprios dados, e juntar no fim.
- No g++ 15 do Ubuntu 26.04,
-pthreadnão é necessário; em sistemas antigos, acrescente.