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 EvenCheckers 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 Generators 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 EvenCheckers 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".