Concorrência e Multithreading em C++: dos fundamentos aos padrões modernos
As aplicações modernas cada vez mais exigem a execução simultânea de várias tarefas. Seja processando requisições de usuários em um servidor, renderizando gráficos ou trabalhando com big data — concorrência e multithreading em C++ se tornaram habilidades obrigatórias para o desenvolvedor. Neste artigo, vamos abordar os principais mecanismos disponíveis na biblioteca padrão do C++, começando com C++11 e terminando com C++20. Você aprenderá como criar threads, sincronizá-las e evitar erros típicos.
Se você já conhece os fundamentos do C++, mas quer se aprofundar em paralelismo — este material é para você. Não vamos nos aprofundar na teoria de sistemas operacionais, mas sim nos concentrar em ferramentas práticas: std::thread, std::mutex, std::async e operações atômicas.
1. Fundamentos do multithreading: std::thread e gerenciamento de threads
No C++11, surgiu o suporte padrão para threads através da classe std::thread. Você pode criar uma nova thread passando um objeto chamável ao construtor — uma função, lambda ou functor.
#include <iostream>#include <thread>
void hello() { std::cout << "Olá da thread " << std::this_thread::get_id() << std::endl;}
int main() { std::thread t(hello); // Criação da thread t.join(); // Aguarda a conclusão da thread return 0;}É importante sempre chamar join() ou detach() para cada thread criada; caso contrário, ao destruir o objeto std::thread, o programa será encerrado abruptamente. join() bloqueia a thread atual até que a thread filha termine, enquanto detach() permite que a thread execute de forma independente.
Para passar argumentos para a thread, use parâmetros adicionais no construtor:
#include <thread>#include <string>
void print_sum(int a, int b, const std::string& msg) { std::cout << msg << ": " << (a + b) << std::endl;}
int main() { std::thread t(print_sum, 3, 4, "Soma"); t.join(); return 0;}Observe: os argumentos são copiados para dentro da thread. Se precisar passar uma referência, use std::ref().
2. Sincronização de threads: mutexes e locks
Ao trabalhar com dados compartilhados de diferentes threads, ocorre uma condição de corrida (data race). Para evitá-la, utiliza-se sincronização. A ferramenta principal é std::mutex.
#include <iostream>#include <thread>#include <mutex>
std::mutex mtx;int shared_counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { mtx.lock(); ++shared_counter; mtx.unlock(); }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Resultado: " << shared_counter << std::endl; return 0;}No entanto, chamar manualmente lock() e unlock() é propenso a erros: se uma exceção ocorrer entre eles, o mutex permanecerá bloqueado. É melhor usar o wrapper RAII std::lock_guard ou std::unique_lock:
void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++shared_counter; } // desbloqueia automaticamente ao sair do escopo}Para um gerenciamento mais flexível (por exemplo, bloqueio condicional), use std::unique_lock. E se precisar sincronizar vários mutexes sem deadlock, utilize std::lock().
3. Tarefas assíncronas: std::async e std::future
Nem sempre é conveniente gerenciar threads manualmente.