|
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»
|