Después de este título tan largo viene una pregunta: ¿conocéis alguna implementación de hilos (threads) orientada a objetos en C++ que sea medianamente decente?
Si la respuesta es sí, genial, decídmelo en los comentarios para echarle un vistazo.
Si la respuesta es no, no hagáis como yo y os dediquéis a reinventar la rueda y usad la implementación de hilos de la librería Boost. En serio. Es más, usad Boost para todo lo que podáis porque tiene infinidad de cosas muy buenas y muy útiles ya probadas. De hecho, varias cosas de esta librería fueron incluidas en el último estándar de C++, C++11 (antes conocido como C++0x).
Pero, en caso de que os guste investigar y queráis implementar vuestra propia librería de gestión de hilos mediante programación orientada a objetos en C++, aquí tenéis un ejemplo de lo que a mi se me ha ocurrido (y que, de momento, funciona, claro
). Y es un gran ladrillo, avisados estáis.
Lo primero que hay que saber
Lo primero que —de forma resumida— hay que saber es que la implementación de los hilos depende de cada sistema, aunque hay una especificación POSIX llamada pthreads que tienen implementada la mayoría de los sistemas operativos.
Esta implementación generalmente está hecha en C mediante programación estructurada, con lo que tenemos un conjunto de funciones a la que se le pasan parámetros para controlar todo el funcionamiento de los hilos.
Algunas de estas funciones son la de crear e iniciar el hilo —cuyo parámetro principal es un puntero a la función que dicho hilo tiene que ejecutar—, parar el hilo, esperar a que termine dicho hilo, etc.
De lo que trata esta entrada es de encapsular esta funcionalidad en clases para controlar los hilos mediante programación orientada a objetos y así tener un API más consistente, más abstracta y, sobre todo, más moderna. Y aquí es donde los defensores a ultranza de la programación estructurada empiezan a gritar…
Bueno, yo creo que cada cosa está para lo que está y la programación estructurada está muy bien para programar kernels, pero cuando se trata de programar aplicaciones de usuario por parte de un desarrollador de este tipo de aplicaciones (no de sistemas) siempre es mejor facilitarle la tarea a dicho desarrollador. Y con la POO se consigue.
La idea principal
Como comenté arriba, la idea principal es encapsular las funciones de C de gestión de hilos en clases y métodos de C++. Simple y sencillo ¿no? Para ello, diseñaremos las clases necesarias para incluirlas. En principio sólo una, la clase Hilo; o, mejor, la clase Thread, porque me gusta más programar en inglés, por eso de que si compartes tu código llegarás a más gente en este idioma, nos guste o no.
Pero, antes de nada, hay que solucionar un problema importante: a la función de creación del hilo hay que pasarle como parámetro un puntero a la función a ejecutar, pero las clases en C++ no tienen funciones sino que tienen métodos. ¿Y cuál es la diferencia? La principal diferencia es que en el cuerpo de las funciones se tienen los parámetros que se declaran en su definición mientras que en los métodos, además de los parámetros definidos, se tiene un parámetro oculto llamado, generalmente, this (en C++, self en Delphi…) que es un puntero a la instancia del objeto que llama a dicho método.
Y, obviamente, no se puede pasar un puntero a un método a una función que requiere como parámetro un puntero a una función.
La solución está en pasar un método estático de nuestra clase Thread a esta función pasándole como parámetro (visible, no oculto) el puntero a la instancia de nuestra clase Thread. Luego veremos un ejemplo de implementación.
Después de abordar este problema, lo siguiente es encapsular las funciones de gestión de hilos. Aquí, la verdad, no es más que tener un método en nuestra clase por cada función de gestión de hilos que nos interese gestionar. Simple, la verdad.
Lo que hay que ejecutar
El problema de pasar un puntero a una función en lugar de un método como parámetro de la función de creación del hilo está solucionado. Ahora nos queda qué es lo que hay que ejecutar. Y también es sencillo. Se puede declarar un método en nuestra clase Thread llamado execute() que sea el método estándar que siempre ejecute nuestra clase, aunque una solución más elegante (que no más eficiente, sólo estamos hablando de elegancia) sería implementar el operador () (llamada de función) y ser ese el que ejecute nuestro hilo. Repito, cuestión de elegancia, nada más.
Pero, aquí llegamos al segundo problema: ¿qué pasa si quiero ejecutar código diferente sin tener que modificar mi clase Thread cada vez?
La primera solución que se nos ocurre, por obvia, es la herencia. Declaramos la función execute() (o el operador ()) como virtual y hacemos clases derivadas donde lo implementamos. Sin duda, una muy buena solución.
Pero, ¿y si, por cualquier razón, queremos o necesitamos que nuestro hilo pueda ejecutar cualquier método de cualquier otra clase —siempre y cuando cumpla con una definición de parámetros común—?
Aquí es donde nuestra primera planificación de una sola clase Thread puede no ser suficiente.
La implementación
La solución que se me ocurrió pasa por tener dos clases. La primera, una clase Thread como la del texto anterior donde estén declarados e implementados todos los métodos que vamos a usar para gestionar el hilo, teniendo declarado el método execute() como virtual para que se puedan heredar más clases que implementen dicho método. A esta clase la llamaremos AbstractThread (por no permitir tener instancias de la misma y obligar a que sea derivada).
La otra clase sería una clase que hereda de AbstractThread a la que, ahora sí, llamaremos Thread, pero que tiene la peculiaridad de que va a usar plantillas de C++, también conocidas como templates, para indicar qué método de qué clase se va a ejecutar en el hilo.
Al final, contaremos con dos clases: AbstractThread, que es derivable y que es el método execute() el que ejecutará el hilo, y Thread, que está basada en plantillas y será a través de ellas y de su constructor desde donde indicaremos el método a ejecutar y la clase a la que pertenece.
El pseudocódigo de la interfaz de las clases
En este pequeño snippet de código muestro la interfaz (que no la implementación) básica de las dos clases antes mencionadas. En un principio la clase AbstractThread de la que heredan las demás implementando el método execute() y luego la clase Thread usando plantillas.
Clase AbstractThread
typedef int thread_id;
class AbstractThread {
private:
/**
* Función estática que es la que se le pasa a la función
* 'pthread_create(...)' para que ejecute. El parámetro
* 'void* arg' será la instancia de la clase cuya función
* 'execute()' hay que ejecutar. Un ejemplo de implementación
* está en el siguiente código.
*/
static void* thread_entry(void* arg) {
// Código de ejemplo:
AbstractThread* thread = reinterpret_cast<AbstractThread*>(arg);
thread->execute();
return NULL;
}
protected:
/**
* Función virtual pura que tienen que implementar las clases
* derivadas y será la que ejecute el hilo.
*/
virtual void execute() = 0;
public:
/**
* Constructor de la clase.
*/
AbstractThread();
/**
* Destructor de la clase.
*/
virtual ~AbstractThread();
/**
* Devuelve el identificador del hilo (el 'handle').
*/
thread_id getId();
/**
* Inicia el hilo.
*/
virtual bool start();
/**
* Para el hilo.
*/
virtual bool stop();
/**
* Indica si el hilo se está ejecutando o no.
*/
bool isRunning();
/**
* Pide al hilo que pare de ejecutarse. Este es un método
* de parada conde la implementación de 'execute()' para
* de forma cooperativa (sin forzar la parada).
*/
virtual bool requestStop();
/**
* Bloquea el hilo que llama a este método hasta que el
* hilo en cuestión finaliza (espera la finalización del
* hilo).
*/
virtual bool wait();
/**
* Similar a 'wait()' pero con un tiempo de espera. Si el
* hilo no finaliza el dicho tiempo de espera, devuelve
* un error.
*/
virtual bool timedWait(int microseconds);
/**
* Finaliza el hilo de forma abrupta.
*/
virtual bool kill(int signum);
/**
* Devuelve la prioridad del hilo.
*/
int priority();
/**
* Fija la prioridad del hilo.
*/
void priority(int prio);
/**
* Función estática que devuelve el identificador
* del hilo que llama a esta función.
*/
static thread_id getCurrentThreadId();
/**
* Función estática que hace que el hilo que llama a
* esta función deje de ejecutarse hasta que vuelva
* a ser planificado por el kernel.
*/
static void yield();
};
Clase Thread usando plantillas
template <class Worker>
class Thread : public AbstractThread {
public:
/**
* Tipo de dato nuevo que representa la definición del método
* a ejecutar en este hilo.
*/
typedef void (Worker::*WorkerMethod)(const Thread<Worker>* thread);
private:
/**
* Puntero a la instancia de la clase cuyo método hay que
* ejecutar. En caso de que no se pase ninguna instancia en
* el constructor, se creará (y destruirá) internamente.
*/
Worker* fWorker;
/**
* Puntero al método a ejecutar dentro de la clase Worker.
*/
WorkerMethod fMethod;
protected:
/**
* Método heredado de la clase AbstractThread que se implementa
* para ejecutar el método del Worker en el hilo.
*/
virtual void execute() {
if(fWorker != NULL && fMethod != NULL) {
(fWorker->*fMethod)(this);
}
}
public:
/**
* Constructor de la clase donde se le pasa como parámetro
* un puntero al método a ejecutar. Como no se pasa instancia
* de la clase a la que pertenece dicho método, se crea una
* internamente (y se destruye al final).
*/
Thread(WorkerMethod method = &Worker::execute);
/**
* Constructor de la clase donde se le pasa una referencia a
* una instancia de la clase de donde hay que ejecutar el método
* y un puntero al método a ejecutar.
*/
Thread(Worker& worker, WorkerMethod method = &Worker::execute);
/**
* Constructor de la clase donde se le pasa un puntero a
* una instancia de la clase de donde hay que ejecutar el método
* y un puntero al método a ejecutar.
*/
Thread(Worker* worker, WorkerMethod method = &Worker::execute);
};
El resto de métodos de la clase Thread serán los mismos que en la clase AbstractThread por lo que no hace falta volver a implementarlos.
Un ejemplo de uso
Un ejemplo del uso de la clase AbstractThread sería la propia clase Thread, donde basta con implementar el método virtual puro execute() para que sea ese código el que se ejecute en el hilo.
Un ejemplo de uso de la clase Thread podría ser:
#include <iostream>
using namespace std;
class Thread;
class MiClase {
public:
void ejecutar(const Thread<MiClase>* thread) {
// Cualquier código a ejecutar en el hilo, por ejemplo:
int counter = 5;
while(counter-- > 0) {
cout << "Hola mundo." << endl;
sleep(1);
}
}
};
MiClase mi_clase* = new MiClase();
Thread<MiClase>* thr = new Thread<MiClase>(mi_clase,&MiClase::ejecutar);
thr->start();
thr->wait();
delete thr;
delete mi_clase;
Algunas notas acerca del código
En los métodos de las clases AbastractThread y Thread se usan valores de retorno para saber si se han ejecutado correctamente o no. Esta decisión está de mano del desarrollador pudiendo usar valores booleanos, números enteros representando códigos de error (usando, por ejemplo, un enum) o excepciones.
Personalmente, aunque la primera implementación que hice fue devolviendo un tipo de dato enumerado representando códigos de error, creo que la mejor forma de hacerlo es usar excepciones debido a que tienen sus ventajas respecto al uso de valores de retorno.
Clases adicionales necesarias
El uso de hilos en las aplicaciones, además de tener que gestionar correctamente la creación de los mismos, implica, al menos, tres cosas más:
- Evitar el acceso a la misma zona de memoria de forma simultánea por dos o más hilos.
- Sincronizar el orden de acceso de dos o más hilos a un recurso compartido.
- Comunicación entre diferentes hilos.
Es por esto que es necesario implementar otras clases (por seguir con el modelo de programación orientada a objetos) con el fin de tener dichos mecanismos.
Por ejemplo, para envitar el acceso a la misma memoria al mismo tiempo habría que implementar una clase Mutex (mutex viene de mutual exclusion) que evita, precisamente, este supuesto, bloqueando el acceso a ciertas zonas de memoria a todos los hilos en caso de que uno de ellos ya esté en dicha zona.
Además, para la sincronización se debería implementar una clase Semaphore que sirve para indicar el orden en que los hilos acceden a los recursos. Además, también podría servir para pasar información (básica) entre hilos.
Y, finalmente, habría que implementar las clases de comunicación teniendo en cuenta los diferentes métodos que se pueden usar como, por ejemplo, la memoria compartida, colas de mensajes, señales, sockets, colas FIFO…
Conclusión
Si estáis implementando aplicaciones en C++ que hagan uso de hilos, como dije más arriba, no os compliquéis la vida y usad la librería Boost. Bien implementada, con una buena interfaz y muy, muy probada.
Pero si, por cualquier motivo, tenéis que implementar vuestra propia librería, bien sea porque queréis aprender o porque no queréis que vuestro programa dependa de otras librerías o porque vuestro jefe os lo ha dicho, podéis probar, si queréis, esta idea que he expuesto aquí y, como no, podéis ponerlo en los comentarios… por compartir más que nada
.
Y tened en cuenta que esto es muy mejorable, pero la implementación que he hecho funciona bastante bien y es la que estoy usando actualmente. Espero que os sirva
.