Trabajo con colas de tareas en C++: de lo simple a lo complejo
En el desarrollo moderno con C++, rara vez se prescinde del procesamiento asíncrono de datos. Las colas de tareas (task queues) son una de las herramientas clave para organizar la ejecución paralela de código. Permiten distribuir el trabajo entre hilos, evitar bloqueos y utilizar eficientemente los recursos del procesador. En este artículo, analizaremos cómo implementar una cola de tareas en C++, examinaremos el patrón productor-consumidor y aprenderemos a construir un thread pool simple.
1. Cola básica basada en std::queue
La cola de tareas más simple es un contenedor en el que se pueden agregar elementos (tareas) y extraerlos en orden FIFO. En C++, std::queue de la biblioteca estándar es perfecta para esto. Sin embargo, al trabajar con múltiples hilos, una cola normal no es segura: el acceso simultáneo desde diferentes hilos provoca condiciones de carrera.
Consideremos un ejemplo básico de una cola sincronizada usando un mutex y una variable de condición:
#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_;};En este código, push() agrega una tarea y notifica a un hilo en espera. pop() bloquea el hilo hasta que la cola deje de estar vacía. Este enfoque resuelve el problema de sincronización, pero no escala a múltiples consumidores sin modificaciones.
2. Patrón Productor-Consumidor
El problema clásico del productor-consumidor (producer-consumer) encaja perfectamente con una cola de tareas. Uno o varios hilos productores generan tareas, y los hilos consumidores las ejecutan. Es importante gestionar correctamente el tamaño de la cola para evitar desbordamientos o tiempos de inactividad.
Ejemplo con múltiples consumidores:
#include <thread>#include <vector>#include <iostream>
void worker(TaskQueue& queue, int id) { while (true) { auto task = queue.pop(); if (!task) break; // señal de finalización std::cout << "Worker " << id << " executing task"; task(); }}
int main() { TaskQueue queue; std::vector<std::thread> workers;
// Iniciamos 4 hilos consumidores for (int i = 0; i < 4; ++i) { workers.emplace_back(worker, std::ref(queue), i); }
// El productor agrega tareas for (int i = 0; i < 10; ++i) { queue.push([i]() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "Task " << i << " done"; }); }
// Señal de finalización (tarea nullptr) for (auto& w : workers) { queue.push(nullptr); }
for (auto& w : workers) { w.join(); } return 0;}Aquí, cada consumidor recibe una tarea, la ejecuta y espera la siguiente. La señal de finalización (nullptr) detiene el bucle. Este patrón se utiliza a menudo en aplicaciones de servidor, manejadores de eventos y motores de juegos.
3. Thread Pool — un pool de hilos para la cola de tareas
Un thread pool (pool de hilos) es una capa superior sobre la cola de tareas que gestiona