Considere el siguiente ejemplo, donde una tarea genera números constantes y otras tareas consumen esos números. Ahora, el único trabajo de los hilos consumidores es probar la validez de los números constantes.
Primeramente, definiremos EvenChecker
,
el hilo consumidor, puesto que será reutilizado en todos los
ejemplos siguientes. Para
desacoplar EvenChecker
de los varios
tipos de generadores con los que experimentaremos, crearemos
una interfaz llamada Generator
que
contiene el número mínimo de funciones
que EvenChecker
necesita conocer: por
lo que tiene una función nextValue() y que puede ser
cancelada.
//: C11:EvenChecker.h #ifndef EVENCHECKER_H #define EVENCHECKER_H #include <iostream> #include "zthread/CountedPtr.h" #include "zthread/Thread.h" #include "zthread/Cancelable.h" #include "zthread/ThreadedExecutor.h" class Generator : public ZThread::Cancelable { bool canceled; public: Generator() : canceled(false) {} virtual int nextValue() = 0; void cancel() { canceled = true; } bool isCanceled() { return canceled; } }; class EvenChecker : public ZThread::Runnable { ZThread::CountedPtr<Generator> generator; int id; public: EvenChecker(ZThread::CountedPtr<Generator>& g, int ident) : generator(g), id(ident) {} ~EvenChecker() { std::cout << "~EvenChecker " << id << std::endl; } void run() { while(!generator->isCanceled()) { int val = generator->nextValue(); if(val % 2 != 0) { std::cout << val << " not even!" << std::endl; generator->cancel(); // Cancels all EvenCheckers } } } // Test any type of generator: template<typename GenType> static void test(int n = 10) { std::cout << "Press Control-C to exit" << std::endl; try { ZThread::ThreadedExecutor executor; ZThread::CountedPtr<Generator> gp(new GenType); for(int i = 0; i < n; i++) executor.execute(new EvenChecker(gp, i)); } catch(ZThread::Synchronization_Exception& e) { std::cerr << e.what() << std::endl; } } }; #endif // EVENCHECKER_H ///:~
Listado 10.16. C11/EvenChecker.h
La clase Generator
presenta la clase
abstracta Cancelable
, que es parte de
la biblioteca de ZThread. El propósito
de Cancelable
es proporcionar una
interfaz consistente para cambiar el estado de un objeto via
cancel() y ver si el objeto ha sido cancelado con la función
isCanceled(). Aquí utilizamos el enfoque simple de una bandera
de cancelación booleana similar a quitFlag, vista previamente
en ResponsiveUI.cpp. Note que en este ejemplo la clase que
es Cancelable
no
es Runnable
. En su lugar, toda
tarea EvenChecker
que dependa de un
objeto Cancelable
(el Generator
) lo comprueba para ver
que ha sido cancelado, como puede ver en run(). De esta
manera, las tareas que comparten recursos comunes
(el Cancelable
Generator
)
estén atentos a la señal de ese recurso para terminar. Esto
elimina la también conocida condición de carrera, donde dos o
más tareas compiten por responder una condición y, así,
colisionar o producir resultados inconsistentes. Debe pensar
sobre esto cuidadosamente y protegerse de todas las formas
posible de los fallos de un sistema concurrente. Por ejemplo,
una tarea no puede depender de otra porque el orden de
finalización de las tareas no está garantizado. En este
sentido, eliminamos la potencial condición de carrera haciendo
que las tareas dependan de objetos que no son tareas (que son
contados referencialmente
utilizando CountedPtr
.
En las secciones posteriores, verá que la librería ZThread contiene más mecanismos generales para la terminación de hilos.
Debido a que muchos objetos EvenChecker
podrían terminar compartiendo
un Generator
, la
plantilla CountedPtr
se usa para contar
las referencias de los
objetos Generator
.
El último método en EvenChecker
es un
miembro estático de la plantilla que configura y realiza una
comprobación de los tipos de Generator
creando un CountedPtr
dentro y,
seguidamente, lanzar un número
de EvenChecker
s que usan
ese Generator
. Si
el Generator
provoca un fallo, test()
lo reportará y volverá; en otro caso, deberá pulsar Control-C
para finalizarlo.
Las tareas EvenChecker
leen
constantemente y comprueban que los valores de
sus Generator
s asociados. Vea que si
generator->isCanceled() es verdadero, run() retorna, con lo
que se le dice al Executor
de
EvenChecker::test() que la tarea se ha
completado. Cualquier tarea EvenChecker
puede llamar a cancel() sobre
su Generator
asociado, lo que causará
que todos los demás EvenChecker
s que
utilicen ese Generator
finalicen con
elegancia.
EvenGenerator
es simple -
nextValue() produce el siguiente valor constante:
//: C11:EvenGenerator.cpp // When threads collide. //{L} ZThread #include <iostream> #include "EvenChecker.h" #include "zthread/ThreadedExecutor.h" using namespace ZThread; using namespace std; class EvenGenerator : public Generator { unsigned int currentEvenValue; // Unsigned can't overflow public: EvenGenerator() { currentEvenValue = 0; } ~EvenGenerator() { cout << "~EvenGenerator" << endl; } int nextValue() { ++currentEvenValue; // Danger point here! ++currentEvenValue; return currentEvenValue; } }; int main() { EvenChecker::test<EvenGenerator>(); } ///:~
Listado 10.17. C11/EvenGenerator.cpp
Es posible que un hilo llame a nextValue() después de el
primer incremento de currentEvenValue y antes del segundo (en
el lugar "Danger point here!" del código comentado), que pone
el valor en un estado "incorrecto". Para probar que esto puede
ocurrir, EventChecker::test() crea un grupo de
objetos EventChecker
para leer
continuamente la salida de
un EvenGenerator
y ver si cada valor es
constante. Si no es así, el error se reporta y el programa
finaliza.
Este programa podría no detectar el problema hasta
que EvenGenerator
ha completado varios
ciclos, dependiendo de las particuliaridades de su sistema
operativo y otros detalles de implementación. Si quiere ver
que falla mucho más rápido, pruebe a poner una llamada a
yield() entre el primero y segundo incremento. En algún
evento, fallará puntualmente a causa de que los
hilos EvenChecker
pueden acceder a la
información en EvenGenerator
mientras
se encuentra en un estado "incorrecto".