Piense en un programa con un único hilo como una solitaria entidad moviéndose a lo largo del espacio de su problema y haciendo una cosa en cada instante. Debido a que sólo hay una entidad, no tiene que preocuparse por el problema de dos entidades intentando usar el mismo recurso al mismo tiempo: problemas como dos personas intentando aparcar en el mismo sitio, pasar por una puerta al mismo tiempo o incluso hablar al mismo tiempo.
Con multihilado las cosas ya no son solitarias, pero ahora tiene la posibilidad de tener dos o más hilos intentando utilizar un mismo recurso a la vez. Esto puede causar dos problemas distintos. El primero es que el recurso necesario podría no existir. En C++, el programador tiene un control total sobre la vida de los objetos, y es fácil crear hilos que intenten usar objetos que han sido destruidos antes de que esos hilos hayan finalizado.
El segundo problema es que dos o más hilos podrían chocar cuando intenten acceder al mismo dispositivo al mismo tiempo. Si no previene esta colisión, tendrá dos hilos intentando acceder a la misma cuenta bancaria al mismo tiempo, imprimir en la misma impresora, ajustar la misma válvula, etc.
Esta sección presenta el problema de los objetos que desaparecen mientras las tareas aún están usándolos y el problema del choque entre tareas sobre recursos compartidos. Aprenderá sobre las herramientas que se usan para solucionar esos problemas.
La gestión de memoria y recursos son las principales preocupaciones en C++. Cuando crea cualquier programa en C++, tiene la opción de crear objetos en la pila o en el heap (utilizando new). En un programa con un solo hilo, normalmente es sencillo seguir la vida de los objetos con el fin de que no tenga que utilizar objetos que ya están destruidos.
Los ejemplos mostrados en este capítulo crean
objetos Runnable
en el heap utilizando
new, se dará cuenta que esos objetos nunca son destruidos
explícitamente. Sin embargo, podrá por la salida cuando
ejecuta el programa que la biblioteca de hilos sigue la pista
a cada tarea y, eventualmente, las destruye. Esto ocurre
cuando el método Runnable::run() finaliza - volver de run()
indica que la tarea ha finalizado.
Recargar el hilo al destruir una tarea es un problema. Ese hilo sabe necesariamente si otro necesita hacer referencia a ese Runnable, y por ello el Runnable podría ser destruido prematuramente. Para ocuparse de este problema, el mecanismo de la biblioteca ZThread mantiene un conteo de referencias sobre las tareas. Una tarea se mantiene viva hasta que su contador de referencias se pone a cero, en este punto la tarea se destruye. Esto quiere decir que las tareas tienen que ser destruidas dinámicamente siempre, por lo que no pueden ser creadas en la pila. En vez de eso, las tareas deben ser creadas utilizando new, tal y como puede ver en todos los ejemplos de este capítulo. tareas in ZThreads
Además, también debe asegurar que los objetos que no son tareas estarán vivos tanto tiempo como el que las tareas necesiten de ellos. Por otro lado, resulta sencillo para los objetos utilizados por las tareas salir del ámbito antes de que las tareas hayan concluido. Si ocurre esto, las tareas intentarán acceder zonas de almacenamiento ilegales y provocará que el programa falle. He aquí un simple ejemplo:
//: C11:Incrementer.cpp {RunByHand} // Destroying objects while threads are still // running will cause serious problems. //{L} ZThread #include <iostream> #include "zthread/Thread.h" #include "zthread/ThreadedExecutor.h" using namespace ZThread; using namespace std; class Count { enum { SZ = 100 }; int n[SZ]; public: void increment() { for(int i = 0; i < SZ; i++) n[i]++; } }; class Incrementer : public Runnable { Count* count; public: Incrementer(Count* c) : count(c) {} void run() { for(int n = 100; n > 0; n--) { Thread::sleep(250); count->increment(); } } }; int main() { cout << "This will cause a segmentation fault!" << endl; Count count; try { Thread t0(new Incrementer(&count)); Thread t1(new Incrementer(&count)); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } } ///:~
Listado 10.14. C11/Incrementer.cpp
Podría parecer a priori que la
clase Count
es excesiva, pero si
únicamente n es un int (en lugar de una matriz), el compilador
puede ponerlo dentro de un registro y ese almacenamiento
seguirá estando disponible (aunque técnicamente es ilegal)
después de que el objeto Count
salga
del ámbito. Es difícil detectar la violación de memoria en
este caso. Sus resultados podrían variar dependiendo de su
compilador y de su sistema operativo, pero pruebe a que n sea
un int y verá qué ocurre. En cualquier evento,
si Count
contiene una matriz de ints y
como antes, el compilador está obligado a ponerlo en la pila y
no en un registro.
Incrementer
es una tarea sencilla que
utiliza un objeto Count
. En main(),
puede ver que las tareas Incrementer
se
ejecutan el tiempo suficiente para que el salga del ámbito,
por lo que la tarea intentará acceder a un objeto que no
existe. Esto produce un fallo en el programa.
objeto Count
Para solucionar este problema, debemos garantizar que cualquiera de los objetos compartidos entre tareas estarán accesibles tanto tiempo como las tareas los necesiten. (Si los objetos no fueran compartidos, podrían estar directamente dentro de las clases de las tareas y, así, unir su tiempo de vida a la tarea.) Dado que no queremos que el propio ámbito estático del programa controle el tiempo de vida del objeto, pondremos el en heap. Y para asegurar que el objeto no se destruye hasta que no haya objetos (tareas, en este caso) que lo estén utilizando, utilizaremos el conteo de referencias.
El conteo de referencias se ha explicado a lo largo del
volumen uno de este libro y además se revisará en este
volumen. La librería ZThread incluye una plantilla
llamada CountedPtr
que automáticamente
realiza el conteo de referencias y destruye un objeto cuando
su contador de referencias vale cero. A continuación, se ha
modificado el programa para que
utilice CountedPtr
para evitar el
fallo:
//: C11:ReferenceCounting.cpp // A CountedPtr prevents too-early destruction. //{L} ZThread #include <iostream> #include "zthread/Thread.h" #include "zthread/CountedPtr.h" using namespace ZThread; using namespace std; class Count { enum { SZ = 100 }; int n[SZ]; public: void increment() { for(int i = 0; i < SZ; i++) n[i]++; } }; class Incrementer : public Runnable { CountedPtr<Count> count; public: Incrementer(const CountedPtr<Count>& c ) : count(c) {} void run() { for(int n = 100; n > 0; n--) { Thread::sleep(250); count->increment(); } } }; int main() { CountedPtr<Count> count(new Count); try { Thread t0(new Incrementer(count)); Thread t1(new Incrementer(count)); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } } ///:~
Listado 10.15. C11/ReferenceCounting.cpp
Ahora Incrementer
contiene un
objeto CountedPtr
, que gestiona
un Count
. En la función main(), los
objetos CountedPtr
se pasan a los dos
objetos Incrementer
por valor, por lo
que se llama el constructor de copia, incrementando el
conteo de referencias. Mientras la tarea esté ejecutándose, el
contador de referencias no valdrá cero, por lo que el
objeto Count
utilizado
por CountedPtr
no será
destruído. Solamente cuando todas las tareas que utilice
el Count
terminen se llamará al
destructor (automáticamente) sobre el
objeto Count
por
el CountedPtr
.
Siempre que tenga una tarea que utilice más de un objeto, casi
siempre necesitará controlar aquellos objetos utilizando la
plantilla CountedPtr
para evitar
problemas derivados del tiempo de vida de los objetos.