Continuando con nuestros trucos de programación de C++, hoy vamos a ver un mecanismo para implementar monitores inteligentes usando plantillas en C++.
Probablemente estarás pensando… ¿qué leches es un monitor inteligente? Bien, la verdad es que no he encontrado un nombre mejor. Con “monitor inteligente” me refiero a un monitor que puede ser empleado para proteger cualquier objeto del acceso concurrente en entornos multihebra. Con “cualquier objecto” me refiero a objetos de cualquier clase, tanto escritos por uno mismo como por terceras partes. Suena interesante, ¿o no?
Aquí tenemos un fragmento de código típicamente problemático.
class IntGenerator {
public:
virtual int generateValue() const = 0;
};
typedef std::set<int> IntSet;
IntGenerator *igen = ...; // initializes a integer generator
IntSet dataset;
void *entrypoint(void *data) {
while (true)
dataset.insert(igen->generateValue());
}
int main() {
pthread_t thr[2];
pthread_create(&thr[0], NULL, entrypoint, NULL);
pthread_create(&thr[1], NULL, entrypoint, NULL);
}
El acceso concurrente a dataset probablemente derive en una inconsistencia en el árbol binario que mantiene internamente dicha variable.
La semántica que queremos obtener es la siguiente. Tenemos una plantilla llamada Monitor que emplearemos de la siguiente forma (incluyo sólo el código modificado):
IntSet dataset;
Monitor<IntSet> mdataset(&dataset);
void *entrypoint(void *data) {
while (true)
mdataset->insert(igen->generateValue());
}
La principal diferencia es que hemos creado un nuevo objeto monitor usando dataset como entrada. Ahora podemos emplear el operador de miembro a través de puntero (o flecha) para invocar la operación insert(). El resultado de esa invocación es thread-safe, es decir, no habrá ninguna otra hebra accediendo a los miembros del conjunto mientras dicha operación es invocada.
¿Dónde está la magia? Bien, es evidente que el monitor mantiene una referencia al objeto monitorizado, la cual es empleada para invocar la operación insert(). Podemos sospechar fácilmente el siguiente código:
template <class T>
class Monitor {
public:
Monitor(T *obj) : mObj(obj) {}
private:
T *mObj;
};
La aparición del operador de acceso a miembro a través de puntero revela su sobrecarga.
T *operator -> () { ... }
¿Qué hay dentro de ese operador? Bien, no tan rápido. Puesto que esta operación proporciona semántica thread-safe, es evidente que no encontraremos el siguiente código.
T *operator -> () { return mObj;}
Es obvio que el siguiente código tampoco aparecerá:
T *operator -> () {
somemutex.lock();
return mObj;
somemutex.unlock();
}
Está claro que podemos incluir cualquier código “prefijo” antes de la sentencia de retorno del valor monitorizado. Sin embargo, no podemos incluir ningún código “sufijo” después de la sentencia return. Os prometo que existe una solución que proporciona la semántica mencionada más arriba. ¿Sabrías decir cual?


