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::moveao manipular objetos pesados dentro da fila para minimizar cópias desnecessárias e maximizar o throughput.