este espacio está libre ¿deseas utilizarlo? haz clic aquí.

   Secciones

   Inicio
   Documentos
   Programas
   Links
   Proyectos
   Libros

   Actualizaciones & Foro

   Avisar al actualizar
   Foro Delphiladero

   Otras

   Contacto
   Agregar a favoritos
   Vota por nosotros

 

 

 

inicio » documentos » intro a la programación multihilo - parte 1


indice

Algunos conceptos previos
      Programación Concurrente, qué es y para qué sirve
      Multitarea y MultiThread
Programando un Thread en Delphi
      TThread
      Usando TThread. El programa RELOJ
      Conclusiones del programa RELOJ
Coordinar varios Threads
      Variables externas al hilo
      Pausa y reanudación
      Prioridad
      Número de hilos


« algunos conceptos previos »


programación concurrente, qué es y para qué sirve

Los programas, normalmente se ejecutan secuencialmente. Cuando el programa tiene que ejecutar una acción muy larga que dure varios segundos como procesar un archivo o factorizar una serie de números, no permite la ejecución de nada que no sea la acción que está realizando en ese momento.

Así el programa deja, por ejemplo, de estar atento a los eventos como la entrada del ratón o teclado, quedándose literalmente colgado. De esta forma, podemos observar que el programa solamente está realizando UNA tarea.

Sin embargo, podemos necesitar que nuestro programa realice varias tareas simultáneamente. Esto plantea nuevos problemas, como la comunicación entre los distintos procesos, o la coordinación de los mismos. En este artículo trataremos de explicar como se pueden lanzar varios hilos (Threads) en una misma aplicación y cómo sincronizarlos usando Borland Delphi (V6).

En la naturaleza, los sucesos ocurren simultáneamente, ya que son independientes unos de otros. Un programa de simulación en el que haya varios factores independientes se implementa más fácilmente de esta forma. También puede servir para desbloquear la E/S cuando se está ejecutando un proceso largo, como una descarga o para realizar varias peticiones simultaneas (Una página web).

Este tipo de programación permite aislar distintas actividades en distintos hilos de ejecución.

Por desgracia, el código es más complejo, lo que complica el programa y obliga a una programación más "correcta".


multitarea y multihilo

Windows, Linux, y otros sistemas operativos permiten la ejecución simultanea de varios programas. A esto se le llama multitarea. Cada tarea, al arrancar, tiene su propia zona de memoria reservada, y funciona independientemente de las demás (Esto no siempre es así, a veces un programa necesita que otro esté corriendo para funcionar correctamente.

No voy a extenderme en cómo hace esto un único procesador, pero baste decir que los procesadores modernos (por ejemplo la serie x86 de INTEL desde el i386), vienen preparados para esto, y que junto al sistema operativo, lo que hacen es distribuir el tiempo del procesador en intervalos pequeños (Quantum), ejecutando un trozo de tarea en cada intervalo y cambiando a la siguiente. Hay algoritmos como el Round Robin, que sirven para asignar prioridades de ejecución de forma dinámica.

Se pueden lanzar varias tareas simultáneamente desde un único programa. Cada proceso que corre paralelamente a los demás recibe el nombre de Thread (Hilo), y un programa con varios Threads recibe el nombre de MultiThread (Multihilo).


~ programando un thread en delphi ~


Tthread

La programación concurrente encaja en la programación orientada a objetos. De esta forma, cada tarea se encaja en un objeto de clase TThead. TThread es una clase abstracta, de forma que cada proceso que creemos, será un descendiente de TThread.

 

Creando un objeto tipo TThread

En el menú Archivo, crearemos un Nuevo Objeto TThread. Yo voy a crear una clase nueva llamada Treloj. Aparecerá lo siguiente:

unit Unit1;

interface

uses
  Classes;

type
  TReloj = class(TThread)
    private
    { Private declarations }
    protected
      procedure Execute; override;
end;

implementation

{ Important: Methods and properties of objects in VCL or CLX can only be used
in a method called using Synchronize, for example,
Synchronize(UpdateCaption); and UpdateCaption could look like,

procedure TReloj.UpdateCaption;
begin
  Form1.Caption := 'Updated in a thread';
end; }

{ TReloj }

procedure TReloj.Execute;
begin
  { Place thread code here }
end;

El código lo podemos copiar y pegar en otra unit, o colocar un uses en la unit que viene con el formulario.


usando Tthread. el programa reloj

Vamos a crear un programa que mida el tiempo. Usaremos la clase Treloj que hemos creado anteriormente. Primero haremos un formulario, en el que incluiremos dos botones y una etiqueta.

Usaremos una de las etiquetas para actualizar el tiempo transcurrido, un botón para iniciar la cuenta y otro para finalizarla. Copiamos la clase Treloj en el código. El formulario debería tener un aspecto semejante a:

Aunque cada uno puede colocar los componentes como quiera. Ahora vamos a ver el código del programa y luego se comentará.

unit Threads;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  Tform_Reloj = class(TForm)
    Marca: TLabel;
    inicia_cuenta: TButton;
    detener_cuenta: TButton;
    procedure detener_cuentaClick(Sender: TObject);
    procedure inicia_cuentaClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
end;

TReloj = class(TThread)
  private
    hora : integer;
    salir : boolean;
    procedure Actualiza_Texto;
  protected
    procedure Execute; override;
    procedure Terminar;
end;

var
  Form_Reloj : TForm_Reloj;
  reloj : Treloj;
  corriendo : boolean;

implementation

{$R *.dfm}

procedure Treloj.Terminar;
begin
  salir:=true; {SALIR DEL BUCLE CERRADO EN Treloj.execute}
end;

Procedure TReloj.Actualiza_texto;
begin
  Form_Reloj.Marca.Caption:=IntToStr (hora); {COPIA LA HORA EN LA ETIQUETA DEL FORMULARIO}
end;

procedure TReloj.Execute;
begin
  salir:=false; {INICIAMOS LA HORA Y UN BUCLE CERRADO}
 
hora:=0;
  while not salir do
  begin
    Synchronize (Actualiza_texto);
    sleep (1000); {ESPERAR UN SEGUNDO ANTES DE LA SIGUIENTE PASADA}
    inc (hora); {INCREMENTAR LA HORA}
  end;
end;

procedure Tform_Reloj.detener_cuentaClick(Sender: TObject);
begin
  if corriendo then {VERIFICAR QUE ESTÁ CORRIENDO}
  begin
  reloj.Terminar; {FINALIZAR LA CUENTA}
  reloj.Free; {LLAMA AL DESTRUCTOR}
  corriendo:=false; {INDICAR QUE NO ESTÁ CORRIENDO YA}
  end;
end;

procedure Tform_Reloj.inicia_cuentaClick(Sender: TObject);
begin
  if not corriendo then {COMPROBAR QUE NO ESTÁ CORRIENDO}
  begin
    reloj:=Treloj.Create(false);{CONSTRUCTOR, LLAMA a Reloj.execute}
    corriendo:=true; {INDICAR QUE YA ESTÁ CORRIENDO}
  end;
end;

procedure Tform_Reloj.FormCreate(Sender: TObject);
begin
  corriendo:=false; {AL PRINCIPIO, NO ESTÁ CORRIENDO}
end;

end.

Lo primero que hay que ver es la clase Treloj. En ella podemos ver que hemos colocado dos variables que son hora y salir. Como no vamos a acceder a ellas directamente, las hemos declarado dentro de la sección private, al igual que el procedimiento actualiza_texto.

El procedimiento Execute es en el que tenemos que escribir el código que va a ejecutarse.

Obsérvese que no es llamada directamente, sino a través del constructor Treloj.Create. En el incrementamos el reloj cada segundo transcurrido.

El uso de la variable "corriendo", que nos permite evitar el llamar dos veces al constructor, o llamar al destructor cuando el proceso no está corriendo. OJO. Si llamásemos varias veces a reloj.Create, llamaríamos a varios procesos que incrementarían sus variables internas y actualizarían la etiqueta.

Evitando la variable corriendo, podríamos hacerlo. Crear varios hilos de esta forma puede resultar necesario para programar determinadas cosas.

El texto de la etiqueta indica los segundos que van corriendo. Se actualiza a través del procedimiento Actualiza_Texto, que no es llamado directamente, sino a través de Synchronize. Esto nos permite evitar conflictos que pueden ocurrir al acceder a un objeto mientras varios hilos (que pueden acceder al mismo objeto) se ejecutan simultáneamente.

Al detener el reloj, detenemos el mismo con reloj.terminar, destruimos el objeto creado empleando Free y por último ponemos corriendo a false.


conclusiones del programa reloj

El código fuente del programa se explica por si mismo. Lo primero que se observa, es que a pesar de que se está ejecutando un ciclo while-do permanentemente (hasta que salir=true), el programa no se queda colgado, pudiéndose mover o redimensionar la ventana, o salir del mismo. Esto ocurre porque el proceso "reloj" está corriendo en paralelo a la aplicación misma. De esta forma, esta puede leer los eventos de e/s mientras se ejecuta el ciclo while-do.


~ coordinar varios threads ~


Lo que se hace en el programa reloj es lo adecuado, si tenemos en cuenta que solamente hemos lanzado un proceso. Sin embargo puede darse el caso en el que necesitemos que varios procesos distintos funcionen simultáneamente y necesiten intercambiar datos. Esto plantea nuevos inconvenientes,

como por ejemplo ¿Cómo intercambiar dichos datos? ¿Cómo hacer que un proceso espere a otro, cuando va muy adelantado?


variables externas al hilo

Nunca se debe escribir directamente sobre otros objetos. Para ello tenemos Synchronize, que como hemos dicho antes, nos permite evitar conflictos. Esto lo hace suspendiendo la ejecución del hilo en el momento en el que se realiza el acceso a memoria.

Si necesitamos un juego de variables iguales en distintos Threads, podemos usar threadvar en lugar de var. Cada una de ellas será una copia distinta para cada hilo, que no podrá ser accedida por los otros hilos. No obstante estas variables plantean problemas cuando se emplean en punteros y variables dinámicas.


pausa y reanudación

Podemos crear un hilo sin que este se ejecute. En el ejemplo anterior, hemos visto como al llamar al constructor reloj.create lo hacemos con el parámetro false. Ese parámetro se llama create_suspended, y si llamásemos a reloj.create (true), el proceso quedaría en pausa.

Para que el proceso se reanudase, necesitaríamos llamar al método resume (reloj.resume).

Si quisiéramos pausarlo de nuevo, deberíamos llamar al método Suspend. Ojo, el método suspend puede ser llamado de forma anidada, de forma que para reanudar necesitaríamos llamar a resume el mismo número de veces que se llamó a suspend.


prioridad

Una tarea puede tener distintas prioridades. Para cambiar la prioridad recurrimos a la propiedad Priority. Esto repercute directamente en el tiempo que el procesador dedica a la tarea. Hay un rango desde la mas baja a la mas alta:

PRIORIDAD  Descripción
tpIdle  Solamente se ejecuta cuando no hay nada mas que hacer.
tpLowest
tpLower
tpNormal  Prioridad normal.
tpHigher
tpHighest
tpTimeCritical  La tarea recibe la mayor prioridad.

Para cambiar la prioridad, es conveniente pausar la tarea, ya que como hemos visto antes, no se debe escribir en los objetos mientras se están ejecutando. Si quisiéramos cambiar la prioridad del reloj, por ejemplo:

reloj.suspend; // Detener la tarea
reloj.priority := tpHigher; // Prioridad mayor a lo normal
reloj.resume; // Reanudar la tarea

Las prioridad con la que se crean los distintos hilos es la normal. OJO, no es conveniente jugar demasiado con las prioridades a menos que tengamos una buena razón para ello, especialmente con la mas alta, ya que puede incluso colgar el sistema.


número de hilos

Hay que tener en cuenta, que un número elevado de hilos ejecutándose simultáneamente supone un mayor gasto de recursos. En el manual de Delphi, se recomienda no usar mas de 16 simultáneos.


Recomienda este documento a un amigo.
Recuerda enviarme tus comentarios sobre el artículo.


«anterior - Indice - siguiente»


 Copyright © Pablo Castagnino 2000-2002. Todos los derechos reservados.


Puedes ayudarnos

¿Conoces algún documento enteramente en español que pueda resultar interesante para nuestra comunidad delphiadicta?

Coméntanos de él aquí.


Vota por nosotros