Introdução: por que a concorrência em C++ não é assustadora, mas sim interessante?
\n\nHá muito tempo, na era dos processadores single-core, os programas eram executados estritamente de forma sequencial. Hoje, até mesmo seu smartphone tem 8 núcleos e os servidores, dezenas. Fazer todos esses núcleos trabalharem para você é onde reside a verdadeira magia do desempenho. E o C++, uma das linguagens mais poderosas e de baixo nível, nos dá todo um arsenal de ferramentas para isso. Mas como não se confundir? Threads, corrotinas (muitas vezes chamadas de "gorrotinas" por analogia com Go), async/await — o que escolher?
\n\nNeste artigo, vamos organizar a concorrência moderna em C++ de forma clara. Você aprenderá quando usar threads clássicas e quando usar as leves corrotinas do C++20, e como o async/await simplifica a programação assíncrona. Vamos nessa!
\n\nClássico: multithreading com std::thread
\n\nVamos começar com o mais óbvio — as threads. std::thread é o acesso direto às threads do sistema operacional. Cada thread é um contexto de execução separado que pode ser executado em um núcleo de CPU diferente.
Exemplo simples: cálculo paralelo
\n\nImagine que você precisa calcular a soma dos quadrados dos números de 1 a 10 milhões. Sequencialmente, é demorado. Vamos paralelizar:
\n\n#include <iostream>\n#include <thread>\n#include <vector>\n#include <numeric>\n\nvoid sum_squares(int start, int end, long long& result) {\n result = 0;\n for (int i = start; i <= end; ++i) {\n result += i * i;\n }\n}\n\nint main() {\n const int N = 10'000'000;\n const int NUM_THREADS = 4;\n std::vector<std::thread> threads;\n std::vector<long long> results(NUM_THREADS, 0);\n \n int chunk = N / NUM_THREADS;\n for (int i = 0; i < NUM_THREADS; ++i) {\n int start = i * chunk + 1;\n int end = (i == NUM_THREADS - 1) ? N : (i + 1) * chunk;\n threads.emplace_back(sum_squares, start, end, std::ref(results[i]));\n }\n \n for (auto& t : threads) {\n t.join();\n }\n \n long long total = std::accumulate(results.begin(), results.end(), 0LL);\n std::cout << "Soma dos quadrados: " << total << std::endl;\n return 0;\n}\n\n\nAqui, criamos 4 threads, cada uma calcula sua parte do intervalo. Os resultados são coletados em um vetor. Tudo simples e previsível. Mas há nuances:
\n\n- \n
- Overhead: criar cada thread é caro (alocação de pilha, chamadas de sistema). Para milhares de tarefas pequenas, isso é ineficiente. \n
- Sincronização: se as threads acessam dados compartilhados, são necessários mutexes, que podem causar bloqueios e reduzir o desempenho. \n
- Quantidade limitada: o número de threads geralmente não deve exceder o número de núcleos da CPU (considerando hyper-threading). \n
Para cálculos pesados e longos, as threads são uma excelente escolha. Mas para operações de E/S (leitura de arquivos, requisições de rede), elas geralmente são excessivas. É aqui que entram as corrotinas.
\n\nRevolução do C++20: corrotinas (gorrotinas)
\n\nAs corrotinas no C++20 não são apenas mais um recurso. É uma mudança fundamental na forma como escrevemos código assíncrono. Ao contrário das threads, as corrotinas são co-rotinas leves que podem suspender sua execução e retomá-la mais tarde, sem bloquear a thread do SO.
\n\nMuitos desenvolvedores as chamam de "gorrotinas" devido à semelhança com as goroutines em Go, embora tecnicamente sejam conceitos diferentes. A principal vantagem: você pode executar centenas de milhares de corrotinas em uma única thread sem perda de desempenho.
\n\nComo funciona?
\n\nUma corrotina é uma função que contém a palavra-chave co_await, co_return ou co_yield. O compilador a transforma automaticamente em uma máquina de estados. Aqui está um exemplo básico:
#include <iostream>\n#include <coroutine>\n#incl