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:
Uso de recursos compartidos
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; } |
MUY IMPORTANTE: En la versión 6 de C++Builder, la llamada al método WaitFor() requiere que la hebra aún exista, por lo que no se debe utilizar conjuntamente con la propiedad FreeOnTerminate , ya que ésta propiedad hace que se destruya la hebra en cuanto termina su ejecución.
|
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.