10.4.2. Acceso no apropiado a recursos

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