1. Por que usar std::async e std::future
Se você acabou de começar a trabalhar com threads, a reação natural ao std::async soa assim: "É mais uma forma de fazer a mesma coisa, só que diferente?". Sim e não. Thread é sobre "iniciar execução em algum lugar em paralelo". E std::async é sobre "iniciar um cálculo e me devolver uma alça pela qual eu recuperarei o resultado depois". Essa "alça" chama-se std::future.
Imagine uma cena do dia a dia: você pediu uma pizza. Thread é como "vou preparar a pizza na cozinha". std::async é como "fiz o pedido da pizza e recebi um número de pedido: com ele eu consulto o status e retiro o resultado depois". O número do pedido é o future. Você não precisa saber como a cozinha funciona por dentro — o importante é que o resultado virá e você pode obtê‑lo de forma controlada.
E uma diferença importante: std::async pode adiar a execução. Ou seja, você pode dizer: "não prepare a pizza agora — prepare quando eu for buscá‑la". Isso já é política de execução.
Assincronicidade é um contrato, não mágica
A palavra async dá a impressão de que tudo será necessariamente paralelo, rápido e bonito. Na prática, em C++ isso não é "mágica", é um contrato entre você e a biblioteca padrão: você dispara uma tarefa, e a biblioteca decide quando e onde executá‑la — dependendo da política escolhida.
Durante a padronização houve muitas discussões sobre nuances de comportamento e formulações relativas a std::async e itens relacionados — até sobre se certas partes da especificação podem ser implementadas da mesma forma em todas as bibliotecas padrão. Isso é um bom sinal: o tema não é trivial e não se deve "adivinhar" o comportamento pelo nome.
Portanto o objetivo aqui é prático: aprender a escrever código que não dependa de adivinhações, e entender quando std::async realmente executa o trabalho em paralelo e quando não.
2. Conceitos básicos e sintaxe
Mini-glossário: callable, tarefa, resultado, future
Antes de escrever código, vamos nomear os termos de forma simples.
Callable é "algo que pode ser chamado". Uma função comum é callable. Uma lambda é callable. Às vezes até um objeto com operator() é callable (mas não entraremos nessa profundidade hoje).
Tarefa é seu callable + seus argumentos: aquilo que você quer executar.
Resultado é o valor que o callable retorna (por exemplo, int), ou apenas o fato de conclusão (void).
std::future<T> é um objeto ligado a uma "célula de resultado" (frequentemente chamada shared state). Você obterá o valor mais tarde, quando estiver pronto.
Para std::async é importante guardar uma ideia: ele inicia o callable e retorna um future. Como exatamente ele inicia depende da política.
(Aliás, até nos relatórios e edições da especificação dá para ver que std::async tem seu lugar na seção de futures — não é uma utilidade secundária, mas parte integral do modelo de resultados.)
Sintaxe básica de std::async: "inicia e recebe um future"
Começamos com um exemplo mínimo: uma função soma dois números, mas a executamos via std::async.
#include <future>
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
std::future<int> f = std::async(std::launch::async, add, 2, 3);
std::cout << f.get() << '\n'; // 5
}
Repare em três pontos.
- Primeiro — incluímos <future>, porque std::async e std::future vivem ali.
- Segundo — passamos explicitamente a política std::launch::async. Por que especificar, se queremos async? Porque sem política você pode se enganar nas expectativas, e esse é o ponto central da palestra.
- Terceiro — get() parece "ok, peguei o valor". Hoje não vamos aprofundar o ciclo de vida do future, mas é importante entender: get() é o ponto onde você realmente obtém o resultado, e se ele não estiver pronto você bloqueia.
3. Políticas de execução std::launch
Agora — a parte mais importante.
std::async aceita políticas de execução indicadas por std::launch.
std::launch::async
Essa política significa: "execute a tarefa de forma assíncrona". Na prática típica isso será uma thread separada (ou uma atividade assíncrona equivalente na implementação).
Ou seja, seu callable começará a executar imediatamente, e a thread principal continuará.
std::launch::deferred
Essa política significa: "adie a execução até que alguém solicite o resultado".
O ponto mais importante: com deferred a tarefa é executada na thread que chamou a espera (get() ou wait()), frequentemente — diretamente em main. Isso não é paralelismo. É execução preguiçosa.
Por que isso pode ser útil? Por exemplo, você quer descrever um cálculo, mas executá‑lo somente se ele realmente for necessário. É parecido com "vou gerar o relatório, mas só se você clicar em 'mostrar relatório'".
Demonstração: deferred — "depois e na mesma thread"
Vamos um exemplo que imprime a ordem dos eventos. Aqui nem precisamos de um resultado — o importante é ver quando ocorre a execução.
#include <future>
#include <iostream>
int main() {
auto f = std::async(std::launch::deferred, [] {
std::cout << "work\n";
return 10;
});
std::cout << "before get\n";
std::cout << f.get() << '\n';
}
Saída esperada:
// before get
// work
// 10
A ideia é: enquanto você não chamar get(), o "trabalho" nem começou.
"Quando é realmente async": a armadilha da política padrão
Aqui começa a parte em que o iniciante costuma dizer: "Eu chamei std::async, então é async!". E a biblioteca padrão responde: "Ha‑ha. Que gracinha".
Se você chama std::async sem política explícita, normalmente recebe uma escolha "ambas as opções são permitidas": a implementação pode executar a tarefa assincronamente ou adiá‑la. Na prática isso significa: em uma plataforma você verá paralelismo, em outra não, e em outra — "às vezes sim, às vezes não".
Por isso é útil adotar a regra: se paralelismo é essencial — declare std::launch::async explicitamente.
Vamos ilustrar isso com uma tarefa "artificialmente longa".
#include <chrono>
#include <future>
#include <iostream>
#include <thread>
int slow() {
std::this_thread::sleep_for(std::chrono::seconds{1});
return 7;
}
int main() {
auto f = std::async(std::launch::async, slow);
std::cout << "doing something...\n";
std::cout << f.get() << '\n'; // 7
}
Se aqui a política fosse deferred, "doing something..." poderia ser impresso, mas slow() só começaria no get(), resultando em uma "pseudo-assincronicidade": parece que você fez algo, mas nada foi calculado em paralelo.
Esquema visual: async vs deferred
Para tirar a mágica do processo, vamos fixar numa forma simples.
flowchart TD
A[main: chama std::async] --> B{política de execução}
B -->|launch::async| C[tarefa inicia imediatamente em outra thread/atividade]
B -->|launch::deferred| D[tarefa NÃO inicia imediatamente]
D --> E["main: depois chama get()/wait()"]
E --> F[tarefa é executada na thread main]
C --> G["main: depois chama get()/wait()"]
G --> H[main obtém o resultado]
F --> H
Esse esquema é seu "anti-misticismo". Se você o entende, não escreverá código no espírito "ah, deve paralelizar".
4. Resultados, tempo de vida e design
std::future<void>: quando o valor não importa, mas a conclusão importa
Às vezes você não precisa de um valor. Você só precisa aguardar que a tarefa tenha terminado. Ex.: "gravar log", "limpar cache", "fechar conexão corretamente".
Nesse caso é conveniente retornar void — e obter um std::future<void>.
#include <future>
#include <iostream>
void work() {
// faz algo útil
}
int main() {
std::future<void> f = std::async(std::launch::async, work);
f.get(); // aguardamos a conclusão; sem valor
std::cout << "done\n"; // done
}
Note que get() para future<void> é "aguardar e verificar que tudo está ok". Não entraremos em exceções de future hoje, mas a moral é: future<void> também é um "canal de resultado", só que o resultado é o fato de ter terminado com sucesso.
Argumentos e tempo de vida: por que async "de repente falha"
Agora um ponto importante que quebra iniciantes com mais frequência do que vírgulas em cout.
std::async inicia o trabalho "depois" ou "em algum lugar paralelo". Logo, tudo que for usado dentro da tarefa deve viver tempo suficiente.
Se você capturou uma referência para uma variável local e a função já terminou — a referência fica pendurada. Isso pode se manifestar como "crash aleatório", "lixo aleatório" ou "funciona no meu computador".
Estilo perigoso — capturar por referência sem garantia de tempo de vida:
#include <future>
#include <string>
int main() {
std::string name = "Alice";
auto f = std::async(std::launch::async, [&] {
return name.size();
});
return 0; // risco: a tarefa pode ainda ler name
}
Estilo mais seguro — capturar por valor:
#include <future>
#include <string>
int main() {
std::string name = "Alice";
auto f = std::async(std::launch::async, [name] {
return name.size();
});
(void)f.get();
}
Sim, é uma cópia. Mas para std::string a cópia é o preço da tranquilidade. Otimizações virão quando o código estiver correto (e quando houver necessidade clara).
Nuance de design: std::async não é "fire-and-forget"
O iniciante tem rápido a tentação: "Vou espalhar std::async pelo programa todo e tudo ficará rápido!". É como distribuir rádios para todos os funcionários e achar que a empresa ficou mais eficiente.
std::async pressupõe que você mantenha o future, ou seja, você tem um ponto onde ou pega o resultado, ou pelo menos aguarda a conclusão. É disciplina: se você "perde" o future, perde o controle sobre a tarefa.
Boas práticas: crie o future → guarde numa variável → posteriormente chame conscientemente get() (ou wait(), que veremos depois).
Pensamento para o futuro: future e a "célula de resultado" compartilhada
Hoje não vamos nos aprofundar no ciclo de vida do future, mas vale saber que internamente existe uma "célula de resultado" e a biblioteca padrão passou anos refinando como essa célula é criada e liberada. Até sobre future::get houve debate sobre o quão explicitamente deveria estar escrito que o estado compartilhado é "desanexado" e liberado.
Isso não é para assustar. É para você entender: future não é "apenas um int numa caixinha". É um objeto com ciclo de vida, e deve ser tratado com disciplina.
5. Exemplo prático: gerador de relatórios em console
Para que os exemplos não sejam um conjunto de "cavalos esféricos no vácuo", vamos montar um pequeno app que seja fácil de estender.
A ideia é simples: temos uma lista de "tasks" (não threads, mas tarefas do projeto), e queremos construir um "relatório" — uma operação cara (na prática: leitura, parsing, agregação). Enquanto o relatório é gerado, a thread principal pode continuar a fazer outras coisas (no exemplo de ensino — ao menos imprimir mensagens).
Passo 1: modelo de dados e função "cara"
#include <chrono>
#include <string>
#include <thread>
#include <vector>
struct Task {
std::string title;
int estimateHours;
};
int buildReport(const std::vector<Task>& tasks) {
std::this_thread::sleep_for(std::chrono::milliseconds{300}); // simulando trabalho
int total = 0;
for (const auto& t : tasks) total += t.estimateHours;
return total;
}
A função retorna um int: a soma de horas estimadas.
Passo 2: iniciar o relatório via std::async com política explícita
#include <future>
#include <iostream>
#include <vector>
int main() {
std::vector<Task> tasks{{"Parse logs", 5}, {"Generate PDF", 2}};
auto futureTotal = std::async(std::launch::async, buildReport, std::cref(tasks));
std::cout << "report started...\n";
int total = futureTotal.get();
std::cout << "total hours = " << total << '\n'; // total hours = 7
}
Aqui aparece um detalhe novo: std::cref(tasks). É uma forma cuidadosa de passar uma referência como argumento (em vez de copiar todo o vector). Se não tiver certeza, pode passar uma cópia — para tamanhos didáticos isso é aceitável. Mas mostro std::cref porque é comum e evita "arrastar um vagão" de dados.
Importante: especificamos explicitamente std::launch::async, porque hoje queremos aprender a não adivinhar se será paralelo.
Outra demonstração: dois cálculos em paralelo
Vamos a um exemplo que dispara dois cálculos. Mesmo sem cronometrar você verá a diferença de abordagem.
#include <chrono>
#include <future>
#include <iostream>
#include <thread>
int slowCalc(int x) {
std::this_thread::sleep_for(std::chrono::milliseconds{200});
return x * 2;
}
int main() {
auto a = std::async(std::launch::async, slowCalc, 10);
auto b = std::async(std::launch::async, slowCalc, 20);
std::cout << a.get() + b.get() << '\n'; // 60
}
Se ambas as tarefas realmente começarem em paralelo, o tempo total ficará mais próximo de 200 ms (mais overhead) do que de 400 ms. Se você usasse deferred, ambas só começariam no get(), e então quase certamente seriam "uma após a outra" (pois get() é chamado sequencialmente).
6. Erros típicos ao trabalhar com std::async e políticas de execução
Erro #1: usar std::async sem política e esperar paralelismo garantido.
Esse código pode "acidentalmente" rodar em paralelo na sua máquina e de repente virar adiado em outra. Se o paralelismo faz parte da lógica (ex.: você conta com sobreposição de I/O/CPU), declare explicitamente std::launch::async, para que o contrato fique claro para você e para o leitor do código.
Erro #2: pensar que std::launch::deferred é "executar após um tempo".
deferred não significa "espere 2 segundos e depois comece". Significa "não comece até que eu peça o resultado". Ou seja, o momento de início é definido não por um temporizador, mas pelo chamado get()/wait().
Erro #3: capturar por referência variáveis locais que podem morrer antes da tarefa.
Essa é a armadilha clássica de lambdas em código concorrente. Você olha a variável, ela parece viva e simpática, e um milissegundo depois main terminou, a variável sumiu e a tarefa ainda tenta acessá‑la. Se não houver garantia firme de tempo de vida — capture por valor ou passe dados de forma que o dono viva mais tempo.
Erro #4: "perder" o future e achar que a tarefa agora vive por conta própria.
std::async não é um detach. Se você não guarda o future, perde o ponto normal de sincronização e observação do resultado. Considere future parte obrigatória do contrato: iniciou a tarefa — guardou a alça — depois conscientemente esperou e/ou pegou o resultado.
Erro #5: tentar colocar tudo dentro de std::async, inclusive trabalho com estruturas de dados compartilhadas
std::async inicia um cálculo, mas não anula as regras de concorrência. Se dentro da tarefa você acessa dados compartilhados, continua precisando de acordos: quem é o dono, quem lê, onde está o mutex, onde usar condition_variable. std::async é uma forma de obter um resultado "depois", não um amuleto universal contra races.
GO TO FULL VERSION