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.