Curso de C++ Builder


Programación con Hebras

Coordinación entre hebras



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.

Uso de recursos compartidos

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!.

La hebra principal de la VCL

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:

  • En ObjGraf.h y en ObjGraf.cpp:
    El método Synchronize() recibe cómo parámetro un método del tipo void __fastcall <método> (void), por lo que hay que cambiar la definición del método Mover() de TPelota:
    void __fastcall Mover (void);
    
  • En HPelota.cpp:
    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).

  • Los objetos gráficos funcionan correctamente con hebras. No se necesita utilizar la hebra VCL principal acceder a TFont, TPen, TBrush, TBitmap, TMetafile ni TIcon. Para compartir el uso de objetos lienzo (TCanvas y descendientes) hay que bloquearlos antes.

  • Algunos componentes (por ejemplo, TList) tienen una versión adecuada para funcionar con varias hebras (TThreadList).

    Bloqueo de objetos

    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:

  • En HPelota.cpp:
    Eliminar la llamada al método Synchronize() realizada desde el método THebraPelota::Execute().
  • En ObjGraf.cpp:
    Tanto en TPelota::Mostrar() como en TPelota::Borrar():
    PaintBox->Canvas->Lock();
    
    ...
    
    PaintBox->Canvas->Unlock();
    
  • Secciones críticas

    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:

  • En ObjGraf.cpp:
    #include <syncobjs.hpp>
    
    TCriticalSection *SC = new TCriticalSection();
    
    // ¡¡¡Habría que destruirla!!!
    
    ...
    
    En TPelota::Mostrar() y en TPelota::Borrar(), eliminar el bloqueo del objeto de tipo TCanvas y emplear en su lugar la sección crítica:
    {
    
      SC->Acquire();
    
      try {
    
        ...
    
      } __finally {
    
        SC->Release();
      }
    }
    
  • Sincronización entre hebras

    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.

    Esperar la finalización de una hebra

    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:

  • En Ppal.cpp:
    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.

    Esperar a que se complete una tarea: Sucesos

    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.


    Índice de la sección