10.3. Utilización de los hilos

Para controlar un objeto Runnable con un hilo, crea un objeto Thread separado y utiliza un pontero Runnable al constructor de Thread. Esto lleva a cabo la inicialización del hilo y, después, llama a run ( ) de Runnable como un hilo capaz de ser interrumpido. Manejando LiftOff con un hilo, el ejemplo siguiente muestra como cualquier tarea puede ser ejecutada en el contexto de cualquier otro hilo:

//: C11:BasicThreads.cpp
// The most basic use of the Thread class.
//{L} ZThread
#include <iostream>
#include "LiftOff.h"
#include "zthread/Thread.h"
using namespace ZThread;
using namespace std;

int main() {
  try {
    Thread t(new LiftOff(10));
    cout << "Waiting for LiftOff" << endl;
  } catch(Synchronization_Exception& e) {
    cerr << e.what() << endl;
  }
} ///:~

Listado 10.3. C11/BasicThreads.cpp


Synchronization_Exception forma parte de la librería ZThread y la clase base para todas las excepciones de ZThread. Se lanzará si hay un error al crear o usar un hilo.

Un constructor de Thread sólo necesita un puntero a un objeto Runnable. Al crear un objeto Thread se efectuará la incialización necesaria del hilo y después se llamará a Runnable::run()

Puede añadir más hilos fácilmente para controlar más tareas. A continuación, puede ver cómo los hilos se ejecutan con algún otro:

//: C11:MoreBasicThreads.cpp
// Adding more threads.
//{L} ZThread
#include <iostream>
#include "LiftOff.h"
#include "zthread/Thread.h"
using namespace ZThread;
using namespace std;

int main() {
  const int SZ = 5;
  try {
    for(int i = 0; i < SZ; i++)
      Thread t(new LiftOff(10, i));
    cout << "Waiting for LiftOff" << endl;
  } catch(Synchronization_Exception& e) {
    cerr << e.what() << endl;
  }
} ///:~

Listado 10.4. C11/MoreBasicThreads.cpp


El segundo argumento del constructor de LiftOff identifica cada tarea. Cuando ejecute el programa, verá que la ejecución de las distintas tareas se mezclan a medida que los hilos entran y salen de su ejecución. Este intercambio está controlado automáticamente por el planificador de hilos. Si tiene múltiples procesadores en su máquina, el planificador de hilos distribuirá los hilos entre los procesadores de forma transparente.

El bucle for puede parecer un poco extraño a priori ya que se crea localmente dentro del bucle for e inmediatamente después sale del ámbito y es destruído. Esto hace que parezca que el hilo propiamente dicho pueda perderse inmediatamente, pero puede ver por la salida que los hilos, en efecto, están en ejecución hasta su finalización. Cuando crea un objeto Thread, el hilo asociado se registra en el sistema de hilos, que lo mantiene vivo. A pesar de que el objeto Thread local se pierde, el hilo sigue vivo hasta que su tarea asociada termina. Aunque puede ser poco intuitivo desde el punto de vista de C++, el concepto de hilos es la excepción de la regla: un hilo crea un hilo de ejecución separado que persiste después de que la llamada a función finalice. Esta excepción se refleja en la persistencia del hilo subyacente después de que el objeto desaparezca.

10.3.1. Creación de interfaces de usuarios interactivas

Como se dijo anteriormente, uno de las motivaciones para usar hilos es crear interfaces de usuario interactivas. Aunque en este libro no cubriremos las interfaces gráficas de usuario, verá un ejemplo sencillo de una interfaz de usuario basada en consola.

El siguiente ejemplo lee líneas de un archivo y las imprime a la consola, durmiéndose (suspender el hilo actual) durante un segundo después de que cada línea sea mostrada. (Aprenderá más sobre el proceso de dormir hilos en el capítulo.) Durante este proceso, el programa no busca la entrada del usuario, por lo que la IU no es interactiva:

//: C11:UnresponsiveUI.cpp {RunByHand}
// Lack of threading produces an unresponsive UI.
//{L} ZThread
#include <iostream>
#include <fstream>
#include <string>
#include "zthread/Thread.h"
using namespace std;
using namespace ZThread;

int main() {
  cout << "Press <Enter> to quit:" << endl;
  ifstream file("UnresponsiveUI.cpp");
  string line;
  while(getline(file, line)) {
    cout << line << endl;
    Thread::sleep(1000); // Time in milliseconds
  }
  // Read input from the console
  cin.get();
  cout << "Shutting down..." << endl;
} ///:~

Listado 10.5. C11/UnresponsiveUI.cpp


Para hacer este programa interactivo, puede ejecutar una tarea que muestre el archivo en un hilo separado. De esta forma, el hilo principal puede leer la entrada del usuario, por lo que el programa se vuelve interactivo:

//: C11:ResponsiveUI.cpp {RunByHand}
// Threading for a responsive user interface.
//{L} ZThread
#include <iostream>
#include <fstream>
#include <string>
#include "zthread/Thread.h"
using namespace ZThread;
using namespace std;

class DisplayTask : public Runnable {
  ifstream in;
  string line;
  bool quitFlag;
public:
  DisplayTask(const string& file) : quitFlag(false) {
    in.open(file.c_str());
  }
  ~DisplayTask() { in.close(); }
  void run() {
    while(getline(in, line) && !quitFlag) {
      cout << line << endl;
      Thread::sleep(1000);
    }
  }
  void quit() { quitFlag = true; }
};

int main() {
  try {
    cout << "Press <Enter> to quit:" << endl;
    DisplayTask* dt = new DisplayTask("ResponsiveUI.cpp");
    Thread t(dt);
    cin.get();
    dt->quit();
  } catch(Synchronization_Exception& e) {
    cerr << e.what() << endl;
  }
  cout << "Shutting down..." << endl;
} ///:~

Listado 10.6. C11/ResponsiveUI.cpp


Ahora el hilo main() puede responder inmediatamente cuando pulse Return e invocar quit() sobre DisplayTask.

Este ejemplo también muestra la necesidad de una comunicación entre tareas - la tarea en el hilo main() necesita parar al DisplayTask. Dado que tenemos un puntero a DisplayTask, puede pensar que bastaría con llamar al destructor de ese puntero para matar la tarea, pero esto hace que los programas sean poco fiables. El problema es que la tarea podría estar en mitad de algo importante cuando lo destruye y, por lo tanto, es probable que ponga el programa en un estado inestable. En este sentido, la propia tarea decide cuando es seguro terminar. La manera más sencilla de hacer esto es simplemente notificar a la tarea que desea detener mediante una bandera booleana. Cuando la tarea se encuentre en un punto estable puede consultar esa bandera y hacer lo que sea necesario para limpiar el estado después de regresar de run(). Cuando la tarea vuelve de run(), Thread sabe que la tarea se ha completado.

Aunque este programa es lo suficientemente simple para que no haya problemas, hay algunos pequeños defectos respecto a la comunicación entre pilas. Es un tema importante que se cubrirá más tarde en este capítulo.