Programação Multithread em C++: Sincronização e Gerenciamento de Tarefas

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

  1. Sanitizadores: Utilize ferramentas como o ThreadSanitizer (-fsanitize=thread) no Clang/GCC para detectar condições de corrida em tempo de execução.
  2. Granularidade de Lock: Mantenha as seções críticas o menor possível para evitar gargalos de performance.
  3. False Sharing: Evite que variáveis alteradas por threads diferentes residam na mesma linha de cache (utilize alignas se necessário).

Tags: cpp Multithreading Concurrency performance standard-library

Publicado em 6-8 19:20 por Thomas