Trabalhando com filas de tarefas em C++: do simples ao complexo
No desenvolvimento moderno em C++, raramente se passa sem processamento assíncrono de dados. As filas de tarefas (task queues) são uma das ferramentas-chave para organizar a execução paralela de código. Elas permitem distribuir o trabalho entre threads, evitar bloqueios e utilizar eficientemente os recursos da CPU. Neste artigo, vamos analisar como implementar uma fila de tarefas em C++, explorar o padrão producer-consumer e aprender a construir um thread pool simples.
1. Fila básica baseada em std::queue
A fila de tarefas mais simples é um contêiner no qual podemos adicionar elementos (tarefas) e removê-los na ordem FIFO. Em C++, o std::queue da biblioteca padrão é perfeito para isso. No entanto, ao trabalhar com multithreading, uma fila comum não é segura: o acesso simultâneo de diferentes threads leva a condições de corrida (data races).
Vamos considerar um exemplo básico de uma fila sincronizada usando mutex e variável de condição:
#include <queue>#include <mutex>#include <condition_variable>#include <functional>
class TaskQueue {public: using Task = std::function<void()>;
void push(Task task) { std::lock_guard<std::mutex> lock(mutex_); queue_.push(std::move(task)); cond_var_.notify_one(); }
Task pop() { std::unique_lock<std::mutex> lock(mutex_); cond_var_.wait(lock, [this]() { return !queue_.empty(); }); Task task = std::move(queue_.front()); queue_.pop(); return task; }
bool empty() const { std::lock_guard<std::mutex> lock(mutex_); return queue_.empty(); }
private: std::queue<Task> queue_; mutable std::mutex mutex_; std::condition_variable cond_var_;};Neste código, push() adiciona uma tarefa e notifica uma thread em espera. pop() bloqueia a thread até que a fila não esteja vazia. Essa abordagem resolve o problema de sincronização, mas não escala para múltiplos consumidores sem modificações.
2. Padrão Producer-Consumer
O problema clássico de producer-consumer (produtor-consumidor) se encaixa perfeitamente em uma fila de tarefas. Uma ou mais threads produtoras geram tarefas, e threads consumidoras as executam. É importante gerenciar corretamente o tamanho da fila para evitar estouro ou inatividade.
Exemplo com vários consumidores:
#include <thread>#include <vector>#include <iostream>
void worker(TaskQueue& queue, int id) { while (true) { auto task = queue.pop(); if (!task) break; // sinal de término std::cout << "Worker " << id << " executing task"; task(); }}
int main() { TaskQueue queue; std::vector<std::thread> workers;
// Iniciamos 4 threads consumidoras for (int i = 0; i < 4; ++i) { workers.emplace_back(worker, std::ref(queue), i); }
// Produtor adiciona tarefas for (int i = 0; i < 10; ++i) { queue.push([i]() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "Task " << i << " done"; }); }
// Sinal de término (tarefa nullptr) for (auto& w : workers) { queue.push(nullptr); }
for (auto& w : workers) { w.join(); } return 0;}Aqui, cada consumidor recebe uma tarefa, a executa e aguarda a próxima. O sinal de término (nullptr) interrompe o loop. Esse padrão é frequentemente usado em aplicações de servidor, manipuladores de eventos e motores de jogos.
3. Thread Pool — pool de threads para fila de tarefas
Thread pool (pool de threads) é uma camada sobre a fila de tarefas que gerencia ```