10.4. Comparición de recursos limitados

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.

10.4.1. Aseguramiento de la existencia de objetos

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.