10.3.2. Simplificación con Ejecutores

Utilizando los Ejecutores de ZThread, puede simplificar su código. Los Ejecutores proporcionan una capa de indirección entre un cliente y la ejecución de una tarea; a diferencia de un cliente que ejecuta una tarea directamente, un objeto intermediario ejecuta la tarea.

Podemos verlo utilizando un Ejecutor en vez de la creación explícita de objetos Thread en MoreBasicThreads.cpp. Un objeto LiftOff conoce cómo ejecutar una tarea específica; como el patrón Command, expone una única función a ejecutar. Un objeto Executor conoce como construir el contexto apropiado para lanzar objetos Runnable. En el siguiente ejemplo, ThreadedExecutor crea un hilo por tarea:

//: c11:ThreadedExecutor.cpp
//{L} ZThread
#include <iostream>
#include "zthread/ThreadedExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;

int main() {
  try {
    ThreadedExecutor executor;
    for(int i = 0; i < 5; i++)
      executor.execute(new LiftOff(10, i));
  } catch(Synchronization_Exception& e) {
    cerr << e.what() << endl;
  }
} ///:~

Listado 10.7. C11/ThreadedExecutor.cpp


Note que algunos casos un Executor individual puede ser usado para crear y gestionar todo los hilos en su sistema. Debe colocar el código correspondiente a los hilos dentro de un bloque try porque el método execute() de un Executor puede lanzar una Synchronization_Exception si algo va mal. Esto es válido para cualquier función que implique cambiar el estado de un objeto de sincronización (arranque de hilos, la adquisición de mutexes, esperas en condiciones, etc.), tal y como aprenderá más adelante en este capítulo.

El programa finalizará cuando todas las tareas en el Executor hayan concluido.

En el siguiente ejemplo, ThreadedExecutor crea un hilo para cada tarea que quiera ejecutar, pero puede cambiar fácilmente la forma en la que esas tareas son ejecutadas reemplazando el ThreadedExecutor por un tipo diferente de Executor. En este capítulo, usar un ThreadedExecutor está bien, pero para código en producción puede resultar excesivamente costoso para la creación de muchos hilos. En ese caso, puede reemplazarlo por un PoolExecutor, que utilizará un conjunto limitado de hilos para lanzar las tareas registradas en paralelo:

//: C11:PoolExecutor.cpp
//{L} ZThread
#include <iostream>
#include "zthread/PoolExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;

int main() {
  try {
    // Constructor argument is minimum number of threads:
    PoolExecutor executor(5);
    for(int i = 0; i < 5; i++)
      executor.execute(new LiftOff(10, i));
  } catch(Synchronization_Exception& e) {
    cerr << e.what() << endl;
  }
} ///:~

Listado 10.8. C11/PoolExecutor.cpp


Con PoolExecutor puede realizar una asignación inicial de hilos costosa de una sola vez, por adelantado, y los hilos se reutilizan cuando sea posible. Esto ahorra tiempo porque no está pagando el gasto de la creación de hilos por cada tarea individual de forma constante. Además, en un sistema dirigido por eventos, los eventos que requieren hilos para manejarlos puede ser generados tan rápido como quiera, basta con traerlos del pool. No excederá los recursos disponibles porque PoolExecutor utiliza un número limitado de objetos Thread. Así, aunque en este libro se utilizará ThreadedExecutors, tenga en cuenta utilizar PoolExecutor para código en producción.

ConcurrentExecutor es como PoolExecutor pero con un tamaño fijo de hilos. Es útil para cualquier cosa que quiera lanzar en otro hilo de forma continua (una tarea de larga duración), como una tare que escucha conexiones entrantes en un socket. También es útil para tareas cortas que quiera lanzar en un hilo, por ejemplo, pequeñas tareas que actualizar un log local o remoto, o para un hilo que atienda a eventos.

Si hay más de una tarea registrada en un ConcurrentExecutor, cada una de ellas se ejecutará completamente hasta que la siguiente empiece; todas utilizando el mismo hilo. En el ejemplo siguiente, verá que cada tarea se completa, en el orden en el que fue registrada, antes de que la siguiente comience. De esta forma, un ConcurrentExecutor serializa las tareas que le fueron asignadas.

//: C11:ConcurrentExecutor.cpp
//{L} ZThread
#include <iostream>
#include "zthread/ConcurrentExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;

int main() {
  try {
    ConcurrentExecutor executor;
    for(int i = 0; i < 5; i++)
      executor.execute(new LiftOff(10, i));
  } catch(Synchronization_Exception& e) {
    cerr << e.what() << endl;
  }
} ///:~

Listado 10.9. C11/ConcurrentExecutor.cpp


Como un ConcurrentExecutor, un SynchronousExecutor se usa cuando quiera una única tarea se ejecute al mismo tiempo, en serie en lugar de concurrente. A diferencia de ConcurrentExecutor, un SynchronousExecutor no crea ni gestiona hilos sobre si mismo. Utiliza el hilo que añadió la tarea y, así, únicamente actúa como un punto focal para la sincronización. Si tiene n tareas registradas en un SynchronousExecutor, nunca habrá 2 tareas que se ejecuten a la vez. En lugar de eso, cada una se ejecutará hasta su finalización y la siguiente en la cola comenzará.

Por ejemplo, suponga que tiene un número de hilos ejecutando tareas que usan un sistema de archivos, pero está escribiendo código portable luego no quiere utilizar flock() u otra llamada al sistema operativo específica para bloquear un archivo. Puede lanzar esas tareas con un SynchronousExecutor para asegurar que solamente una de ellas, en un tiempo determinado, está ejecutándose desde cualquier hilo. De esta manera, no necesita preocuparse por la sincronización del recurso compartido (y, de paso, no se cargará el sistema de archivos). Una mejor solución pasa por sincronizar el recurso (lo cual aprenderá más adelante en este capítulo), sin embargo un SynchronousExecutor le permite evitar las molestias de obtener una coordinación adecuada para prototipar algo.

//: C11:SynchronousExecutor.cpp
//{L} ZThread
#include <iostream>
#include "zthread/SynchronousExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;

int main() {
  try {
    SynchronousExecutor executor;
    for(int i = 0; i < 5; i++)
      executor.execute(new LiftOff(10, i));
  } catch(Synchronization_Exception& e) {
    cerr << e.what() << endl;
  }
} ///:~

Listado 10.10. C11/SynchronousExecutor.cpp


Cuando ejecuta el programa verá que las tareas son lanzadas en el orden en el que fueron registradas, y cada tarea se ejecuta completamente antes de que la siguiente empiece. ¿Qué es lo que no ve y que hace que no se creen nuevos hilos? El hilo main() se usa para cada tarea, y debido a este ejemplo, ese es el hilo que registra todas las tareas. Podría no utilizar un SynchronousExecutor en código en producción porque, principalmente, es para prototipado.