Una aplicación multihebra es una aplicación que contiene varías vías simultáneas de ejecución.
Consideraciones:
El uso de hebras (de paralelismo, en general) proporciona una serie de ventajas frente a las limitaciones de los sistemas monotarea.
Ejemplo:
Supongamos que nuestra aplicación tiene que ocuparse de la realización de copias de seguridad de los datos con los que trabaja. Con una única hebra tendríamos que programar las copias de seguridad fuera del horario habitual de trabajo (¿y si tiene que funcionar las 24 horas del día?). Con varias hebras, podemos aprovechar los períodos de inactividad del sistema. |
Cuando se utiliza una sola hebra, el programa debe detener completamente la ejecución mientras espera a que se realice cada tarea. La CPU permanece ocupada completamente (o inactiva) hasta que el proceso actual termine. Si se utilizan varias hebras, el sistema puede usarse para realizar varias tareas simultáneamente (vg: reproducción de MP3s en background).
Como es lógico, se le asigna mayor prioridad a las tareas más importantes (vg: las que requieran una respuesta más rápida).
En un sistema multiprocesador, si la aplicación se descompone en varias hebras, el sistema operativo podrá asignar cada una a una de las CPUs del sistema.
En muchas ocasiones, un programa puede diseñarse como varios procesos paralelos que funcionen de forma independiente. La descomposición de una aplicación en varias hebras puede resultar muy beneficiosa en términos de desacoplamiento de código (tareas independientes se implementan por separado) y de calidad del diseño (frente a futuras ampliaciones, por ejemplo).
IMPORTANTE: El objetivo principal del uso de hebras es mejorar el rendimiento del sistema. El diseñador/programador deberá decidir hasta qué punto deben utilizarse. |
C++ Builder proporciona la clase abstracta TThread para la creación de hebras.
Para definir una hebra se debe crear un descendiente de TThread, para lo cual se ha de crear una clase derivada de TThread. Para facilitar el trabajo, C++Builder incluye un asistente que lo hace por nosotros.
Ejemplo: Crear una hebra
Abrimos el repositorio en su página New (al que se accede desde la opción del menú File->New...). Seleccionamos el elemento Thread Object. Denominamos THebraPelota a la clase derivada de TThread. Ya hemos creado nuestra primera hebra en una unidad independiente (que almacenaremos en los ficheros HPelota.cpp y HPelota.h). |
La clase TThread es abstracta porque posee un método virtual puro denominado Execute(), que será el que rellenemos con el código asociado a la hebra.
Como sucede con cualquier otra clase, en una clase derivada de TThread se pueden añadir todos los miembros (propiedades y métodos) que se necesiten. Obligatoriamente sólo hay que implementar:
La inicialización de una hebra se realiza en su constructor, donde se establecen los valores iniciales de cuantas propiedades sean necesarias.
Antes de invocar al constructor de una clase derivada de TThread, se invoca al constructor de su clase base (TThread):
__fastcall TThread(bool CreateSuspended);
Éste recibe como parámetro false si deseamos que la hebra se ejecute inmediatamente después de ser creada o true si queremos que quede suspendida inicialmente.
Dos características de las hebras conviene establecerlas en el constructor: su prioridad y cuándo debe liberarse la hebra.
La prioridad de una hebra indica qué grado de preferencia tiene la hebra cuando el sistema operativo reparte el tiempo de CPU entre las distintas hebras que se estén ejecutando en el sistema.
Para indicar la prioridad del objeto de hebra hay que fijar el valor de la propiedad Priority:
Valor | Prioridad |
tpIdle | Se ejecuta cuando el sistema está inactivo. |
tpLowest | Dos puntos por debajo del valor normal. |
tpLower | Un punto por debajo del valor normal. |
tpNormal | La hebra tiene la prioridad normal. |
tpHigher | Un punto por encima del valor normal. |
tpHighest | Dos puntos por encima del valor normal. |
tpTimeCritical | La hebra tiene la prioridad más alta. |
Si se aumenta mucho la prioridad de una hebra que requiera muchos recursos (vg: CPU) puede ocurrir que las demás hebras de la aplicación (y del sistema) no lleguen a procesarse a la velocidad adecuada.
Ejemplo:
Si tenemos un proceso que consume una cantidad considerable de tiempo de CPU, puede ser interesante dar la posibilidad al usuario de cancelar el proceso durante su ejecución. Esto se puede conseguir creando una hebra con prioridad normal para ese proceso y otra hebra de alta prioridad que únicamente esté a la espera de que el usuario pulse un botón de cancelación. La hebra de alta prioridad no consumirá tiempo de CPU porque estará bloqueada pero, en el caso de que el usuario pulse el botón, responderá rápidamente. |
A menudo las hebras de una aplicación se ejecutan una sola vez. Si este es el caso, lo más sencillo es dejar que el objeto hebra se libere a sí mismo, estableciendo la propiedad FreeOnTerminate a true.
Sin embargo, hay casos en que el objeto hebra representa una tarea que debe realizarse varias veces. Si la aplicación requiere varias instancias del mismo objeto hebra (para ejecutar la hebra varias veces), se puede aumentar el rendimiento de la aplicación almacenando las hebras en una caché para utilizarlas más adelante, en lugar de destruirlas y crearlas de nuevo cada vez. Para ello, basta con fijar el valor de la propiedad FreeOnTerminate a false. En este caso, será responsabilidad del programador la liberación de la hebra.
MUY IMPORTANTE: Antes de liberar una hebra hay que asegurarse de que no se esté ejecutando (ya veremos cómo). |
Ejemplo:
#include "ObjGraf.h"En la clase THebraPelota:... public: TPelota *Pelota; __fastcall THebraPelota(TPaintBox *PaintBox); __fastcall ~THebraPelota(void); ... #include <stdlib.h> ... __fastcall THebraPelota::THebraPelota (TPaintBox *PaintBox) : TThread(false) { Priority = tpNormal; FreeOnTerminate = true; Pelota = new TPelota ( // Objeto pelota... PaintBox, // ...aletorio. (TColor)(random(0xFF) | (random(0xFF)*0x100) | (random(0xFF)*0x10000)), random(PaintBox->Width), random(PaintBox->Height), random(20)+1, TDireccion((random(2)+1) | (random(2)+1)*4)); } __fastcall THebraPelota::~THebraPelota(void) { delete Pelota; } |
El método Execute es la función principal de una hebra. Una hebra puede concebirse como un programa que se ejecuta dentro de la aplicación. Una hebra no es un programa independiente porque comparte el mismo espacio con las demás hebras de la aplicación. Por tanto, es necesario asegurarse de no escribir en la memoria que utilizan las otras hebras. Por otro lado, puede utilizarse la memoria compartida entre las hebras para establecer una comunicación entre ellas.
Ejemplo:
void __fastcall THebraPelota::Execute() { while (true) { // Código de la Hebra. Pelota->Mover(); Sleep(100); } } |
Con la palabra clave __thread podemos definir variables globales que son locales a una hebra. Habrá una copia de la variable para cada una de las hebras que se creen: la variable es global para las funciones de una hebra pero no se comparte con otras hebras.
Ejemplo:
int __thread x; |
Las variables __thread han de ser estáticas (no pueden ser punteros) y no pueden inicializarse en tiempo de ejecución).
No es aconsejable utilizar este tipo de variables, ya que se puede conseguir lo mismo añadiendo una variable de instancia a la clase de la hebra.
Una hebra puede encontrarse en dos estados:
Las hebras pueden iniciarse y suspenderse tantas veces como sea necesario antes de que finalice su ejecución. Para detener una hebra temporalmente hay que realizar una llamada al método Suspend(). Cuando resulte conveniente reanudar la ejecución de la hebra se debe realizar una llamada al método Resume(). Suspend() y Resume() utilizan un contador interno, de forma que es posible anidar las llamadas a Suspend() y Resume(). La hebra no reanuda su ejecución hasta que todas las suspensiones hayan tenido su correspondiente llamada a Resume().
La ejecución de una hebra termina normalmente cuando termina la ejecución del método Execute().
Para hacer que la ejecución de una hebra finalice de forma prematura hay realizar una llamada al método Terminate(). Terminate() cambia el valor de la propiedad Terminated a true, indicando así que la hebra ha de finalizar su ejecución en cuanto sea posible. Por tanto, el método Execute() ha de comprobar periódicamente el valor de la propiedad Terminated y detener la ejecución de la hebra cuando esté a true.
Ejemplo:
void __fastcall THebraPelota::Execute() { while (!Terminated) { // Código de la Hebra. Pelota->Mover(); Sleep(100); } } |
Cuando se crea una hebra, el estado inicial de ésta dependerá del parámetro que se le pase a su constructor: ejecutándose (false
) o suspendida (true
). Si se crea una hebra inicialmente suspendida se
ha de invocar al método Resume() para comenzar su ejecución.
Ejemplo:
Para usar la unidad HPelota desde un formulario:#include "HPelota.h" ... private: // User declarations THebraPelota **Objs; ... #include <stdlib.h> ... void __fastcall TPpalFrm::FormCreate(TObject *Sender) { Objs = new THebraPelota*[4]; randomize(); for (int i=0; i<4; ++i) Objs[i] = new THebraPelota (PaintBox); } void __fastcall TPpalFrm::FormDestroy(TObject *Sender) { for (int i=0; i<4; ++i) Objs[i]->Terminate(); Sleep(3000); // Esperar tres segundos... // ...a que se liberen todas las hebras delete[] Objs; } Nota: La sentencia |
El código ejecutado por una hebra debe tener en cuenta la posible existencia de otras hebras que se ejecuten concurrentemente. Hay que tener cuidado para evitar que dos hebras accedan a un recurso compartido (vg: objeto o variable global) al mismo tiempo. Además, la ejecución de una hebra puede depender del resultado de las tareas que realicen en otras hebras. Por tanto, las distintas hebras de una aplicación han de coordinar su ejecución.
Cuando varias hebras comparten el uso de un recurso, pueden ocurrir situaciones no deseadas si dos o más hebras acceden (o intentan acceder) al mismo recurso simultáneamente.
Para evitar conflictos con otras hebras, puede que se necesite bloquear la ejecución de otras hebras al acceder a objetos o variables compartidas. Hay que tener cuidado de no bloquear innecesariamente la ejecución de otras hebras (para no disminuir el rendimiento de la aplicación).
Ejemplo:
Si ejecutamos el ejemplo anterior, aunque el funcionamiento de la aplicación es correcto, la visualización en el PaintBox sufre algunos errores de forma aleatoria. Esto se debe a que los métodos Mostrar() y Borrar() de las distintas hebras se entrelazan en el acceso al Canvas del PaintBox. ¡Se ha de evitar el acceso simultáneo de las hebras al componente Canvas del PaintBox!. |
No existe garantía alguna (en general) de que los métodos de los componentes de C++ Builder funcionen correctamente cuando son compartidos por varias hebras. Es decir, al acceder a propiedades o ejecutar métodos pueden efectuarse algunas operaciones que utilicen memoria no protegida de las acciones de otras hebras.
Por este motivo se reserva una hebra, la “hebra principal de la VCL”, para el acceso a los objetos de la VCL. Ésta es la hebra que gestiona todos los mensajes de Windows que reciben los componentes de la aplicación.
Si todos los objetos acceden a sus propiedades e invocan a sus métodos dentro de una única hebra, no hay por qué preocuparse. Para usar la hebra VCL principal hay que crear un método que realice las acciones necesarias y llamarlo utilizando el método Synchronize().
Ejemplo:
El método Synchronize() recibe cómo parámetro un método del tipo void __fastcall THebraPelota::Execute() { while (!Terminated) { Synchronize(Pelota->Mover); Sleep(100); } } |
No siempre es necesario utilizar la hebra VCL principal (de hecho, en algunos casos no se puede usar y tendremos que emplear otros mecanismos como las secciones críticas). Algunos componentes están preparados para funcionar con hebras (son thread-safe) y no hay necesidad de usar el método Synchronize(). Esto permite aumentar el rendimiento de la aplicación porque no es necesario esperar a que la hebra VCL principal entre en su bucle de mensajes (y, además, no bloqueamos esta hebra). Los componentes de acceso a datos funcionan correctamente con hebras si pertenecen a distintas sesiones (con la excepción de los controladores de Access, que utilizan la biblioteca ADO).
TFont
, TPen
, TBrush
, TBitmap
, TMetafile
ni TIcon
. Para compartir el uso de objetos lienzo (TCanvas
y descendientes) hay que bloquearlos antes.
TList
) tienen una versión adecuada para funcionar con varias hebras (TThreadList
).
Algunos componentes cuentan con mecanismos de bloqueo para que puedan compartirse por varias hebras. Por ejemplo, los objetos de tipo lienzo (TCanvas
y sus descendientes) cuentan con un método Lock()
que impide a las otras hebras acceder al objeto hasta que se realiza una llamada al método Unlock()
. En el caso de TThreadList
, una llamada a TThreadList::LockList()
devuelve el objeto de tipo TList
asociado e impide que otras hebras accedan a la lista hasta que se realice una llamada a UnlockList()
. Las llamadas a los métodos TCanvas::Lock
o TThreadList::LockList
pueden anidarse con
seguridad. El desbloqueo no se produce hasta que se haya realizado una llamada de desbloqueo por cada llamada de bloqueo.
Ejemplo:
Eliminar la llamada al método Tanto en |
Si un objeto no cuenta con mecanismos de bloqueo incorporados, siempre se puede utilizar una sección crítica. Las secciones críticas funcionan como puertas que permiten el paso de una sola hebra cada vez. Para utilizar una sección crítica hay que crear una instancia global de TCriticalSection
. TCriticalSection
cuenta con dos métodos: Acquire()
(que impide a otras hebras acceder a la sección crítica) y Release()
(que elimina el bloqueo).
Cada sección crítica se asocia a un recurso que se desea compartir por varias hebras. Todas las hebras que accedan a un mismo recurso deberán utilizar el método Acquire()
de la correspondiente sección crítica para asegurarse de que el recurso no está siendo utilizado por ninguna otra hebra. Al finalizar sus operaciones, las hebras tienen que realizar una llamada al método Release()
para que las demás hebras puedan acceder al recurso invocando al método Acquire()
. Si se omite la llamada a Release()
el recurso quedaría bloqueado para siempre.
Ejemplo:
#include <syncobjs.hpp> TCriticalSection *SC = new TCriticalSection(); // ¡¡¡Habría que destruirla!!! ...En |
Puede que las distintas hebras de una aplicación realicen tareas que no sean completamente independientes, por lo cual será necesario que en ciertas ocasiones una hebra espere a que otra termine la ejecución de una acción determinada.
Ejemplo:
Tenemos que dos hebras que calculan el total de ingresos y el total de gastos de una compañía respectivamente. Una tercera hebra ha de obtener el balance final, por lo que deberá esperar a que las dos hebras anteriores terminen su trabajo. |
Para esperar a que finalice la ejecución de otra hebra, se puede utilizar el método WaitFor()
de la otra hebra. WaitFor()
bloquea la ejecución de la hebra actual hasta que la otra hebra se haya cerrado, ya sea por haber finalizando la ejecución de su método Execute()
o por haberse producido una excepción.
WaitFor()
devuelve un valor entero que es el valor de la propiedad ReturnValue
de la hebra. La propiedad ReturnValue
puede cambiarse en el método Execute()
y su significado se lo daremos nosotros.
Ejemplo:
void __fastcall TPpalFrm::FormDestroy(TObject *Sender) { int i; for (i=0; i<4; i++) Objs[i]->Terminate(); for (i=0; i<4; i++) Objs[i]->WaitFor(); delete[] Objs; } |
En algunas ocasiones es necesario esperar a que otra hebra haya realizado una tarea determinada sin que ello implique que haya finalizado su ejecución. En estos casos hay que utilizar un objeto de tipo suceso (TEvent
).
Cuando una hebra completa una operación de la que dependen otras hebras, realiza una llamada a TEvent::SetEvent()
. SetEvent()
activa una señal que cualquier otra hebra puede detectar invocando al método TEvent::WaitFor()
. Para desactivar esa señal existe el método TEvent::ResetEvent()
.
El método WaitFor()
espera un tiempo determinado (establecido en milisegundos al llamar al método) hasta que se active la señal. WaitFor()
puede devolver uno de los siguientes valores:
Valor | Significado |
wrSignaled | La señal asociada al suceso se ha activado. |
wrTimeout | Ha transcurrido el tiempo especificado sin que la señal se activase. |
wrAbandoned | El objeto de tipo TEvent se destruyó antes de que se llegase al time-out. |
wrError | Se ha producido un error durante la espera. El código del error concreto se
puede obtener de la propiedad TEvent::LastError . |
Si se desea seguir a la espera de un suceso indefinidamente puede pasar el valor INFINITE
como parámetro del método WaitFor()
. Pero tenga cuidado, la hebra se quedará bloqueada para siempre si nunca llegara a producirse el evento que espera.
NOTA: El componente TEvent
no es más que un wrapper de un evento de Windows.