Fundamentos do Gerenciamento de Threads
O C++ moderno oferece abstrações poderosas para execução paralela através da biblioteca <thread>. A criação de uma unidade de execução pode ser feita passando funções, lambdas ou objetos fucnionais.
#include <iostream>
#include <thread>
#include <vector>
void executar_logica(int id_processo) {
std::cout << "Processando tarefa: " << id_processo << std::endl;
}
int main() {
// Inicialização via função nomeada
std::thread worker_1(executar_logica, 101);
// Inicialização via expressão Lambda
std::thread worker_2([](int valor) {
std::cout << "Calculando valor dinâmico: " << valor * 2 << std::endl;
}, 50);
// Sincronização obrigatória para evitar terminação abrupta
if (worker_1.joinable()) worker_1.join();
if (worker_2.joinable()) worker_2.join();
return 0;
}
Para garantir que uma thread seja finalizada corretamente mesmo em caso de exceções, recomenda-se o uso do padrão RAII (Resource Acquisition Is Initialization):
class GerenciadorThread {
std::thread t;
public:
explicit GerenciadorThread(std::thread t_) : t(std::move(t_)) {}
~GerenciadorThread() {
if (t.joinable()) t.join();
}
// Impedir cópia para evitar duplicidade de controle
GerenciadorThread(const GerenciadorThread&) = delete;
GerenciadorThread& operator=(const GerenciadorThread&) = delete;
};
Sincronização com Mutex e Prevenção de Deadlocks
O acesso concorrente a dados compartilhados exige mecanismos de exclusão mútua. O C++17 introduziu o std::scoped_lock, que simplifica a aquisição de múltiplos mutexes simultaneamente, reduzindo o risco de deadlocks.
#include <mutex>
std::mutex mtx_recurso_a;
std::mutex mtx_recurso_b;
int contador_global = 0;
void atualizacao_segura() {
// Bloqueia múltiplos mutexes de forma atômica
std::scoped_lock lock(mtx_recurso_a, mtx_recurso_b);
contador_global++;
// O desbloqueio ocorre automaticamente ao sair do escopo
}
Comunicação entre Threads com Variáveis de Condição
Variáveis de condição permitem que threads esperem por sinais específicos ou mudanças de estado, evitando o "busy-waiting" (espera ocupada).
#include <condition_variable>
#include <queue>
std::queue<int> buffer_dados;
std::mutex mtx_buffer;
std::condition_variable sinalizador;
void produtor_evento(int valor) {
{
std::lock_guard<std::mutex> trava(mtx_buffer);
buffer_dados.push(valor);
}
sinalizador.notify_one();
}
void consumidor_evento() {
std::unique_lock<std::mutex> trava(mtx_buffer);
// O predicado (lambda) protege contra despertares espúrios
sinalizador.wait(trava, []{ return !buffer_dados.empty(); });
int dado = buffer_dados.front();
buffer_dados.pop();
trava.unlock();
std::cout << "Evento processado: " << dado << std::endl;
}
Operações Atômicas e Modelo de Memória
Para contadores e flags simples, std::atomic oferece performance superior por evitar travas de exclusão mútua pesadas no nível do sistema operacional.
#include <atomic>
std::atomic<long> sequencial_global{0};
void incrementar_atomo() {
for(int i = 0; i < 10000; ++i) {
// Operação atômica sem necessidade de mutex
sequencial_global.fetch_add(1, std::memory_order_relaxed);
}
}
Diferentes ordens de memória (memory_order) controlam como as instruções podem ser reordenadas pelo compilador ou processador:
memory_order_relaxed: Apenas garante a atomicidade da operação.memory_order_acquire/release: Garante a visibilidade de escritas entre threads.memory_order_seq_cst: Consistência sequencial total (padrão mais seguro e lento).
Implementação de um Pool de Threads Eficiente
Em sistemas de alta performance, criar e destruir threads constantemente é custoso. Um Pool de Threads mantém threads vivas para processar uma fila de tarefas.
#include <functional>
#include <future>
class PoolTrabalho {
std::vector<std::thread> operários;
std::queue<std::function<void()>> tarefas;
std::mutex mtx_fila;
std::condition_variable cond_tarefa;
bool encerrar = false;
public:
PoolTrabalho(size_t quantidade) {
for(size_t i = 0; i < quantidade; ++i) {
operários.emplace_back([this] {
while(true) {
std::function<void()> tarefa;
{
std::unique_lock<std::mutex> lock(mtx_fila);
cond_tarefa.wait(lock, [this]{ return encerrar || !tarefas.empty(); });
if(encerrar && tarefas.empty()) return;
tarefa = std::move(tarefas.front());
tarefas.pop();
}
tarefa();
}
});
}
}
template<class F>
void postar_tarefa(F&& f) {
{
std::lock_guard<std::mutex> lock(mtx_fila);
tarefas.emplace(std::forward<F>(f));
}
cond_tarefa.notify_one();
}
~PoolTrabalho() {
{
std::lock_guard<std::mutex> lock(mtx_fila);
encerrar = true;
}
cond_tarefa.notify_all();
for(std::thread &t : operários) t.join();
}
};
Paralelismo de Alto Nível: std::async e std::shared_mutex
Para tarefas que retornam valores futuros, std::async abstrai a complexidade do gerenciamento de threads.
auto promessa = std::async(std::launch::async, []() {
return 2024;
});
// Bloqueia até que o resultado esteja pronto
int resultado = promessa.get();
Quando há muitos leitores e poucos escritores, o std::shared_mutex (C++17) otimiza a performance permitindo acessos simultâneos de leitura.
#include <shared_mutex>
std::shared_mutex rw_mtx;
int config_global = 42;
void leitor() {
std::shared_lock trava_leitura(rw_mtx); // Permite múltiplos leitores
std::cout << config_global;
}
void escritor(int novo_valor) {
std::unique_lock trava_escrita(rw_mtx); // Bloqueio exclusivo
config_global = novo_valor;
}
Estratégias de Depuração e Boas Práticas
- Sanitizadores: Utilize ferramentas como o ThreadSanitizer (
-fsanitize=thread) no Clang/GCC para detectar condições de corrida em tempo de execução. - Granularidade de Lock: Mantenha as seções críticas o menor possível para evitar gargalos de performance.
- False Sharing: Evite que variáveis alteradas por threads diferentes residam na mesma linha de cache (utilize
alignasse necessário).