Concorrência e Multithreading em C++: dos Fundamentos às Técnicas Avançadas
Os aplicativos modernos exigem cada vez mais desempenho máximo, especialmente ao trabalhar com processadores multi-core. C++ oferece ferramentas poderosas para trabalhar com concorrência e multithreading, desde C++11 até os padrões mais recentes. Neste artigo, abordaremos aspectos avançados de multithreading: desde o uso correto de mutexes e operações atômicas até padrões complexos de sincronização. O material é voltado para desenvolvedores já familiarizados com os fundamentos de threads em C++.
1. Gerenciamento de Threads: std::thread e std::jthread
A classe std::thread é a base da programação multithread em C++. No entanto, em um nível avançado, é importante não apenas criar uma thread, mas também gerenciar corretamente seu ciclo de vida. Com C++20, surgiu o std::jthread, que se junta automaticamente ao ser destruído, prevenindo vazamentos de recursos.
Exemplo de uso de std::jthread:
#include <iostream>#include <thread>#include <chrono>
void worker(int id) { std::this_thread::sleep_for(std::chrono::milliseconds(100 * id)); std::cout << "Thread " << id << " finalizada";}
int main() { // std::jthread se junta automaticamente ao sair do escopo std::jthread t1(worker, 1); std::jthread t2(worker, 2); std::jthread t3(worker, 3); // Não é necessário chamar join() ou detach() explicitamente // Ao destruir t1, t2, t3, main aguarda sua conclusão std::cout << "Thread principal continua trabalhando"; return 0;}É importante lembrar: std::jthread também suporta o mecanismo de parada via std::stop_token, permitindo encerrar com segurança tarefas longas em segundo plano.
2. Sincronização de Acesso: Mutexes, Locks e Variáveis de Condição
O principal problema do multithreading são as condições de corrida. Para evitá-las, usamos mutexes. Em um nível avançado, recomenda-se usar std::scoped_lock (C++17) para capturar vários mutexes sem risco de deadlock.
Exemplo com std::scoped_lock:
#include <iostream>#include <mutex>#include <thread>#include <vector>
std::mutex mtx1, mtx2;int shared_data = 0;
void safe_increment(int id) { // scoped_lock captura ambos os mutexes em ordem, evitando deadlock std::scoped_lock lock(mtx1, mtx2); ++shared_data; std::cout << "Thread " << id << " incrementou o valor para " << shared_data << "";}
int main() { std::vector<std::jthread> threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(safe_increment, i); } return 0;}Para cenários de sincronização mais complexos (por exemplo, produtor-consumidor), são usadas variáveis de condição (std::condition_variable). Elas permitem que as threads aguardem a ocorrência de um evento específico, sem desperdiçar recursos em espera ativa.
3. Operações Atômicas e o Modelo de Memória C++
Operações atômicas (std::atomic) são a base da programação lock-free. Elas garantem que a leitura e a escrita de uma variável sejam executadas como uma única ação indivisível. No entanto, em um nível avançado, é importante entender o modelo de memória C++ e os níveis de ordenação (memory ordering).
Principais níveis de memory ordering:
memory_order_relaxed— garantias mínimas, apenas atomicidadememory_order_acquire— garante que todas as leituras e escritas subsequentes vejam os resultados das operações anterioresmemory_order_release— garante que todas as operações anteriores sejam visíveis após esta escritamemory_order_acq_rel— combinação de acquire e release