Construindo uma Fila Thread-Safe em C++: Implementação e Sincronização de Dados

Uma fila é uma estrutura de dados fundamental baseada no princípio FIFO (First-In, First-Out), onde o primeiro elemento a entrar é obrigatoriamente o primeiro a ser removido. Em sistemas computacionais, essa estrutura é essencial para gerenciar buffers de rede, escalonamento de processos e sistemas de mensageria assíncrona. Contudo, em ambientes de alta performance com múltiplas threads, a implementação simples de uma fila pode falhar devido a condições de corrida (race conditions).

1. Implementação Básica: A Fila Circular

Para evitar o custo computacional de deslocar todos os elementos de um array durante a remoção, utilizamos a fila circular. Nela, os índices de início e fim "giram" dentro do espaço alocado.

template <typename T>
class FilaCircular {
private:
    T* _buffer;
    size_t _capacidade;
    size_t _inicio;
    size_t _fim;
    size_t _tamanho_atual;

public:
    FilaCircular(size_t cap) : _capacidade(cap), _inicio(0), _fim(0), _tamanho_atual(0) {
        _buffer = new T[_capacidade];
    }

    ~FilaCircular() { delete[] _buffer; }

    bool enfileirar(const T& valor) {
        if (_tamanho_atual == _capacidade) return false;
        _buffer[_fim] = valor;
        _fim = (_fim + 1) % _capacidade;
        _tamanho_atual++;
        return true;
    }

    bool desenfileirar(T& saida) {
        if (_tamanho_atual == 0) return false;
        saida = _buffer[_inicio];
        _inicio = (_inicio + 1) % _capacidade;
        _tamanho_atual--;
        return true;
    }
};

2. O Desafio da Concorrência

Quando duas threads tentam inserir um elemento simultaneamente em uma fila não protegida, elas podem ler o mesmo índice de _fim, resultando em perda de dados ou corrupção de memória. Para resolver isso, precisamos de mecanismos de sincronização fornecidos pela biblioteca padrão do C++ (<mutex> e <condition_variable>).

Sincronização com Mutex

A forma mais simples de garantir a segurança é envolver as operações críticas em um std::mutex. Isso garante que apenas uma thread manipule a estrutura interna por vez.

#include <queue>
#include <mutex>
#include <optional>

template <typename T>
class FilaSincronizada {
private:
    std::queue<T> _fila;
    mutable std::mutex _mtx;

public:
    void push(T valor) {
        std::lock_guard<std::mutex> trava(_mtx);
        _fila.push(std::move(valor));
    }

    std::optional<T> pop() {
        std::lock_guard<std::mutex> trava(_mtx);
        if (_fila.empty()) return std::nullopt;
        T valor = std::move(_fila.front());
        _fila.pop();
        return valor;
    }
};

3. Implementando uma Fila Bloqueante (Blocking Queue)

Em um modelo produtor-consumidor, muitas vezes queremos que a thread consumidora "durma" enquanto a fila estiver vazia, em vez de ficar verificando constantemente (busy waiting). Utilizamos std::condition_variable para este propósito.

#include <condition_variable>

template <typename T>
class FilaBloqueante {
private:
    std::queue<T> _dados;
    std::mutex _monitor;
    std::condition_variable _cv;

public:
    void produzir(T item) {
        {
            std::lock_guard<std::mutex> lock(_monitor);
            _dados.push(std::move(item));
        }
        _cv.notify_one(); 
    }

    T consumir() {
        std::unique_lock<std::mutex> lock(_monitor);
        _cv.wait(lock, [this] { return !_dados.empty(); });
        
        T item = std::move(_dados.front());
        _dados.pop();
        return item;
    }
};

4. Aplicação Prática: Sistema de Log Assíncrono

Uma aplicação comum para filas thread-safe é o processamento de logs. As threads de execução postam mensagens na fila e uma thread dedicada de E/S escreve no disco ou console, evitando que o processamento principle sofra latência de escrita.

#include <iostream>
#include <thread>
#include <string>
#include <atomic>

class GerenciadorLogs {
private:
    FilaBloqueante<std::string> _mensagens;
    std::thread _worker;
    std::atomic<bool> _ativo;

    void processar() {
        while (_ativo || !_mensagens.vazia()) {
            // No caso real, adicionaríamos timeout ou verificação de saída
            std::string msg = _mensagens.consumir();
            std::cout << "[LOG]: " << msg << std::endl;
        }
    }

public:
    GerenciadorLogs() : _ativo(true) {
        _worker = std::thread(&GerenciadorLogs::processar, this);
    }

    ~GerenciadorLogs() {
        _ativo = false;
        // Inserir mensagem vazia para desbloquear a thread no wait()
        _mensagens.produzir("Encerrando logger...");
        if (_worker.joinable()) _worker.join();
    }

    void registrar(const std::string& texto) {
        _mensagens.produzir(texto);
    }
};

5. Considerações sobre Performance e Segurança

  • Granularidade da Trava: O uso de um único mutex para toda a fila é simples, mas pode se tornar um gargalo em sistemas com centenas de threads. Para cenários extremos, consideram-se filas lock-free baseadas em operações atômicas (CAS - Compare-And-Swap).
  • Spurious Wakeups: O uso de um predicado (a expressão lambda) dentro do cv.wait é obrigatório para proteger contra despertares espúrios, onde a thread acorda sem que uma notificação real tenha ocorrido.
  • Semântica de Movimento: Sempre utilize std::move ao manipular objetos pesados dentro da fila para minimizar cópias desnecessárias e maximizar o throughput.

Tags: cpp Multithreading DataStructures Concurrency

Publicado em 7-19 13:00