Multihilo 


Hola les pongo este articulo sobre hilos que me mando Javier (Gracias) basado en las notas de Francisco Charte Ojeda autor de la Biblia de Delphi (http://www.fcharte.com). Trae ejemplos al final.

 

¿Qué son las  aplicaciones multihilo?

 

Explicado muy básicamente: son aplicaciones que ejecutan simultáneamente varios procedimientos. Es decir, en lugar de ejecutarse de principio a fin de manera secuencial, el hilo principal, que es por donde comienza la ejecución, crea otros hilos paralelos para ejecutar otros procedimientos.

Aparentemente todos ellos se ejecutan de forma simultánea, lo cual es muy útil en según que casos.

 

 

Ejemplo del sentido de una aplicación

utilizando varios hilos . . . . .
 

Imaginemos que estamos creando una aplicación que va a ejecutarse en un servidor para atender peticiones de clientes. Esta aplicación podría ser un servidor de bases de datos, como Interbase, o un servidor Web, como el Internet Information Server. Cuando se ejecuta el programa éste abre su puerto y queda a la escucha, esperando recibir peticiones. Si cuando recibe una petición de un cliente se pone a procesarla para obtener una respuesta y devolverla, cualquier petición que reciba mientras tanto no podrá atenderla, puesto que está ocupado. La solución es construir la aplicación con múltiples hilos de ejecución.

 

Al ejecutar la aplicación se pone en marcha el hilo principal (esto es siempre igual para cualquier programa, sea o no multihilo), que queda a la escucha. Cuando recibe una petición lo que hace es crear un nuevo hilo que se encarga de procesarla y generar la consulta, mientras tanto el hilo principal sigue a la escucha recibiendo peticiones y creando hilos. De esta manera un gestor de bases de datos puede atender consultas de varios clientes, o un servidor web puede atender a miles de clientes.

 

 

La clase TThread . . .

 

Es una clase abstracta que permite la creación de hilos separados de ejecución en una aplicación. Crea un descendiente de TThread  para representar un hilo de ejecución en una aplicación multi-hilo. Cada nueva instancia de un descendiente de TThread es un nuevo hilo de ejecución. Múltiples instancias de una clase derivada de TThread hacen una aplicación multi-hilo. Cuando una aplicación es lanzada, se carga dentro de memoria lista para su ejecución. En este punto se convierte en un proceso que contiene uno o más hilos que contienen datos, código y otros recursos del sistema para el programa. Un hilo ejecuta una parte de la aplicación y tiene un tiempo de CPU asignado por el S.O. Todos los hilos de un proceso comparten el mismo espacio de direcciones y pueden acceder a variables globales del proceso.

 

Usa hilos para mejorar el rendimiento de una aplicación si:

Por ejemplo, una prioridad alta para manejar tareas de tiempo crítico y una prioridad baja para otras tareas.

 

Lo siguiente son cuestiones y recomendaciones para darse cuenta de cuando usar hilos:

 

    

Para crear y usar un nuevo objeto hilo:

 

 

Métodos, propiedades y eventos importantes

 

Execute

 Método abstracto que contiene el código que se ejecuta cuando el hilo es lanzado.

Create

Crea una instancia de un objeto thread. Si el parámetro es TRUE lo crea pero no lo ejecuta y si es FALSE lo crea y lo ejecuta.

constructor Create(CreateSuspended: Boolean);

Synchronize

Ejecuta la llamada a un método dentro de un hilo principal. Hace que la llamada especificada sea ejecutada usando el thread principal, impidiendo el conflicto entre múltiples hilos. Los métodos y propiedades de objetos en componentes visuales solo pueden ser usados dentro de la llamada al método Synchronize. Si no estas seguro de que la llamada a un método sea un tread seguro, llamaló  desde dentro del método Synchronize para asegurarte que se ejecuta en el thread principal. La ejecución del hilo actual es suspendida mientras el método se ejecuta en el thread principal. No llames a Synchronize desde dentro del thread principal porque esto puede causar un bucle infinito.

 

Terminate Método que pone la propiedad Terminated a True.
OnTerminate es un evento que ocurre después de que el método Execute del hilo termina y antes de que el hilo sea destruido.
Suspend.

Un hilo puede ser arrancado y parado múltiples veces antes de que termine su ejecución. Para parar a un hilo temporalmente se utiliza Suspend.

Resume Rearranca la ejecución de un hilo suspendido.

 

 

 Asignación de prioridades . . .

 

La prioridad indica cuanta preferencia tiene el hilo cuando el S.O. planifica el tiempo de CPU entre todos los hilos de la aplicación. Usa una prioridad alta para tareas criticas y una prioridad baja para el resto de las tareas. Para indicar la prioridad de tu objeto hilo ponlo en la propiedad Priority.

 

Si escribes una aplicación windows, los valores de Priority están en una escala, que es descrita en la siguiente tabla:

 

Valor

Prioridad

tpIdle

El hilo sólo ejecuta cuando el sistema esta parado. Windows no interrumpirá a otros hilos para ejecutar un hilo con prioridad tpIdle.

tpLowest

La prioridad del hilo está dos puntos por debajo de la normal.

tpLower

La prioridad del hilo está un punto por debajo de la normal.

tpNormal

El hilo tiene una prioridad normal.

tpHigher

La prioridad del hilo está un punto por encima de la normal.

tpHighest

La prioridad del hilo está dos puntos por encima de la normal.

tpTimeCritical 

El hilo tiene la prioridad más alta.

 

Si la prioridad de un hilo varia dependiendo de cuando se ejecute, crea el hilo en estado suspendido, pon la prioridad y arranca la ejecución del hilo.

 

SecondThread :=  TMyThread.Create(True);  { lo crea pero no lo ejecuta }

SecondThread.Priority := tpLower;  { pone la prioridad más baja que la normal }

SecondThread.Resume;  { ahora ejecuta el hilo }

 

Si quieres escribir código de inicialización para tu nueva clase hilo, debes sobrescribir el método Create. Añadir un nuevo constructor para la declaración de la clase hilo y escribir el código de inicialización como su implementación. Aquí es donde puedes asignar una prioridad por defecto al hilo e indicar si debería liberado automáticamente cuando su ejecución termine.

 

 

Secciones críticas . . .

 

Funcionan como puertas que permiten entrar a un solo hilo cada vez. Para usar una sección crítica crea una instancia global de tipo TCriticalSection. TCriticalSection tiene dos métodos:

Acquire: que bloquea otros hilos para la ejecución de la sección.

Release: que elimina el bloqueo.

 

Cada sección crítica esta asociada con la memoria global que se quiere proteger. Todo hilo que accede a la memoria global debería primero usar el método Acquire para asegurarse de que ningún otro hilo la use. Cuando termine, el hilo llama al método Release para que así otros hilos puedan acceder a la memoria global llamando a Acquire.

Ej:

LockXY.Acquire; { lock out other threads }

try

  Y := sin(X);

finally

  LockXY.Release;

end;

 

 

Depuración de aplicaciones multi-hilo . . .

 

Cuando depurando aplicaciones multi-hilos, puede ser confuso intentar guardar el estado de todos los hilos que se están ejecutando simultáneamente o incluso determinar que hilo se está ejecutando cuando paramos en un breakpoint. Podemos usar la caja de estado del hilo para seguir la pista y manipular todos los hilos de la aplicación. Para mostrar la caja de estado del hilo elegir View/Debug window/threads del menú principal.

 

Cuando ocurre un evento de depuración (breakpoint, exception, paused), la vista de estado del hilo indica el estado de cada hilo. Pincha con el botón derecho en la caja del estado del hilo para acceder a comandos que localizan el correspondiente recurso o actualizan un hilo diferente. Cuando un hilo es marcado como actual, el siguiente paso u operación a ejecutar será relativa a ese hilo.

 

La caja de estado de hilos lista todos los hilos de ejecución de la aplicación por sus identificadores. Si tu estas usando objetos hilo, el identificador de hilo es el valor de la propiedad ThreadID. Si no usas objetos hilo, el ThreadID para cada hilo es devuelto por la llamada a BeginThread.

 

 


 

Ejemplo práctico de como efectuar un desarrollo con distintos hilos en ejecución

 

Coloca un componente ShellComboBox  (ficha Samples), en su propiedad Root ponle C:\. Coloca un componente ShellListView el cual también se encuentra en la ficha Samples, colócalo debajo del ShellComboBox, su propiedad ViewStyle ponla en vsReport y en su propiedad Root ponle c:\  esto con la finalidad de que nos muestre por defecto el contenido de el directorio raíz C. Puedes dejarlo como sale por defecto eso no es tan importante. En la propiedad ShellComboBox elige ShellComboBox1, por medio de esto estaremos ligando estos dos componentes de tal forma que cuando se cambie de ruta cualquiera de los dos, se actualicen automáticamente.

 

Imaginemos que tenemos un programa que consta de un formulario en el que hay un ShellListView y algunos controles más. La idea es que el usuario seleccione del ShellListView los archivos sobre los que quiere ejecutar un cierto proceso que para el caso que nos ocupa da igual.

El tema es que el contenido del ShellListView debería mostrarse siempre actualizado: este es el problema.

El contenido de la carpeta que está mostrando el ShellListView puede verse alterado por muchas causas: el usuario puede, desde el Explorador de Windows, eliminar, copiar, renombrar archivos; un proceso aparte puede hacer lo mismo, el sistema puede volverse loco y alterar la carpeta, etc.


¿Cuál podría ser la solución?


Casi todo el mundo opta por lo mismo: pongo un TTimer en el formulario, por ejemplo con una cadencia de un segundo, y en el evento OnTimer llamo al método Refresh del ShellListView. De esta forma la lista se está actualizando cada segundo.


¿Funciona?, sí. Pero, ciertamente, es un gasto inútil de recursos. El proceso se repite continuamente cuando seguramente no es preciso. Además, el evento OnTimer se produce siempre y cuando el proceso actual, es decir, el programa, no esté ocupado en otra tarea. Es decir, si se está ejecutando un proceso complejo, un bucle o algo similar el evento OnTimer no se generará y la lista puede quedarse sin actualizar.


En la API de Windows existen unas funciones que permiten algo así como espiar una carpeta para saber cuándo ha sufrido alguna modificación. Estas funciones son FindFirstChangeNotification() y FindNextChangeNotification(). Buscándolas en el archivo de ayuda se ven los parámetros necesarios, que son entendibles. Lo menos claro de estas funciones es, valga la redundancia, su funcionamiento. Ambas devuelven un identificador, nada más.

¿Para qué sirve ese identificador? Pues básicamente para pasarlo como parámetro a la función WaitForSingleObject(), generalmente facilitando como segundo parámetro la constante INFINITE. De esta forma WaitForSingleObject() no devuelve el control hasta el momento en que se ha detectado un cambio en la carpeta, quedando el proceso mientras tanto "dormido", sin consumir prácticamente recurso alguno.


Lógicamente, si llamamos a WaitForSingleObject() desde el formulario principal del programa, que contiene el ShellListView, lo que conseguimos es que éste detenga su funcionamiento hasta que se registre algún cambio en la carpeta que se inspecciona. Esto quiere decir que la interfaz no responderá y el programa no podrá hacer nada, está "sleeping".


La solución es alojar la tarea de espionaje de la carpeta en un hilo de ejecución separado. Este hilo estará durmiendo la mayor parte del tiempo, tan sólo despertará cuando se detecte un cambio en la carpeta. En ese momento se limitará a llamar al método Refresh del ShellListView que hay en el formulario, volviendo a quedar dormido. De esta forma el ShellListView siempre estará actualizado, la interfaz siempre responderá y, además, no se consumirán recursos inútilmente como se hace utilizando un TTimer.


Muy bonito, ¿verdad? Pero, ¿cómo se hace en la práctica?. Veámoslo.


Comienzo abriendo un nuevo proyecto e inserto en el formulario el ShellListView y cualquier otro control que pueda necesitar.

Me voy al Depósito de objetos, con la opción New del menú File, y añado al proyecto un nuevo objeto TThread, al que voy a llamar VigilanteUnidad. Se abre este módulo de código y hago las siguientes cosas:


- Añado a la parte protected de la clase la línea "Procedure Actualiza;", un procedimiento que implementaré de la siguiente forma:

Procedure VigilanteUnidad.Actualiza;
Begin
  Form1.ShellListView.Refresh;
End;

Como se puede ver simplemente llamo al método Refresh del ShellListView que hay en el Form1. Por cierto, para que esta referencia sea válida hay que añadir la correspondiente cláusula Uses (en la parte de implementación: Uses unit1).

Me voy al método Execute(), que es el punto de inicio del TThread, y lo dejo así:

procedure VigilanteUnidad.Execute;
Var
  hNotif: THandle;
begin
  hNotif := FindFirstChangeNotification('C:\',False,
  FILE_NOTIFY_CHANGE_FILE_NAME Or   FILE_NOTIFY_CHANGE_DIR_NAME);
 
  While True do  // Ejecutar siempre
  Begin
    WaitForSingleObject(hNotif, INFINITE);  // Esperamos que se produzca una modificación
    Synchronize(Actualiza);  // Actualizamos la lista
    FindNextChangeNotification(hNotif);  // y esperamos la siguiente notificación
  End;
end;
 

Como se puede ver, llamo a FindFirstChangeNotification() indicando que inspeccione cualquier cambio de nombre de archivo o nombre de carpeta en "C:\". Lógicamente el camino podría ser una variable, por ejemplo el contenido de la propiedad Root del ShellListView. A continuación se inicia un bucle en el que se espera que se produzca una modificación, con WaitForSingleObject(). Cuando la modificación se produce llamamos a Actualiza, aunque de una forma un poco especial. A continuación buscamos la siguiente modificación y el bucle se repite. Tened en cuenta que al llamar a WaitForSingleObject() el proceso queda dormido, es decir, el bucle no está repitiéndose constantemente.

El mayor problema de tener varios hilos de ejecución se plantea a la hora de acceder a cualquier componente de la VCL, ya que es posible que se generen conflictos. Ten en cuenta que en este simple ejemplo hay dos hilos ejecutándose simultáneamente y que ambos podrían, en un determinado momento, intentar modificar la misma propiedad o llamar al mismo método del mismo componente VCL. En la práctica esto puede complicarse si no son dos hilos sino algunos más.

La solución consiste en llamar a los métodos que acceden a componentes VCL usando el método Synchronize(), que se asegurará de que no hay otro hilo accediendo al mismo recurso.

Ya tenemos el módulo TThread preparado, pero falta ponerlo en marcha. Nos vamos ahora al formulario y hacemos doble clic sobre él para abrir el método asociado al evento OnCreate.

Introducimos el código siguiente:

 

procedure TForm1.FormCreate(Sender: TObject);
begin
  Vigilante := VigilanteUnidad.Create(False);
  { ShellListView1.Root := 'C:\'; }
end;

Vigilante es una variable de tipo TThread. Creamos un objeto VigilanteUnidad pasando como parámetro el valor False, lo que le indica que no debe iniciarse en estado de pausa. Acto seguido asignamos a la propiedad Root del ShellListView el camino "C:\".

Por último abrimos el evento OnClose y escribimos lo siguiente:

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Vigilante.Terminate;
end;
 

Con esto destruimos el hilo de ejecución.


Si ahora ejecutamos el programa podremos ver que el ShellListView siempre muestra el contenido actual de la carpeta "C:\", aunque ésta sea modificada externamente, incluso desde un ordenador remoto de la red.


Complementariamente podrían añadirse al formulario un CheckBox o un par de botones que permitiesen suspender/reanudar el hilo de ejecución VigilanteUnidad. Esto es sencillo, basta con llamar a los métodos Suspend()/Resume() del objeto al que apunta la variable Vigilante.


Si tienes algún comentario o deseas compartir alguna información da Click Aquí.....