Boletín Delphi #1
INDICE
1. UNAS PALABRAS DEL EDITOR
2. HILOS DE EJECUCION (THREADS)
- Introducción
- La clase TThread
- Ejemplo de TThread
3. EJECUCION DE UNA APLICACION EXTERNA
4. ¿QUE SIGUE?
________________________________________________________________________
1. UNAS PALABRAS DEL EDITOR
Tengo el placer de anunciar que la semana pasada tuvimos un gran éxito
con el lanzamiento de nuestro Kylix Newsletter, y ahora esperamos que
este Delphi Newsletter siga el mismo camino.
Este boletín es una publicación no oficial destina a programadores en
Delphi con un nivel intermedio. Asumimos que el lector ya tiene cierta
familiaridad con la VCL, el lenguaje Object Pascal y el IDE de Delphi,
de modo tal que iremos directamente a aquellas tareas que un programador
frecuentemente necesita realizar y para las cuales la información
contenida en la documentación que acompaña Delphi es escasa (o incluso
inexistente).
La primera edición estaba programada para mediados de Junio, pero ha
generado tanta expectativa que hemos optado por hacer un esfuerzo y
acceder así a las demandas de nuestros suscriptores de lanzarla mucho
antes de lo planeado. Esperamos que la disfruten.
Este boletín está protegido por la propiedad intelectual, pero es muy
importante para nosotros mantenerlo creciendo en audiencia y contenido
para continuar este proyecto, así que por favor siéntase libre de
reenviarlo a amigos, conocidos y colegas, que Usted crea que pueden
estar interesados en esta publicación, siempre y cuando lo haga en forma
completa y sin modificaciones.
En este número le mostraremos cómo escribir una aplicación multi-hilos o
multi-hebras (del inglés multi-threaded). Este artículo responde a una
tarea muy común en programación, aunque estamos conscientes que quizás
es demasiado técnico, por lo que hemos tratado de escribirlo tan expli-
cativamente como sea posible, esperando que los programadores más
principiantes puedan al menos entender los conceptos básicos del multi-
hilado. Si tiene dudas, problemas o preguntas acerca del ejemplo que
aquí se presenta, por favor envíenos un email. Estaremos complacidos de
ayudarlo en todo lo que esté a nuestro alcance.
Para compensar, también trataremos en este número un tema de menor difi-
cultad, como lo es la ejecución de otras aplicaciones desde la nuestra.
Estamos muy contentos con esta primera edición y esperamos ansiosamente
sus comentarios. Nos gustaría mucho conocer sus opiniones acerca de este
boletín para poder mejorarlo. También nos gustaría conocer cuales son
sus necesidades de programación para evaluar si podemos cubrirlas en las
futuras ediciones. ¡Manténgase en contacto!
Atentamente,
Ernesto De Spirito
eds2008 @ latiumsoftware.com
________________________________________________________________________
JfControls Lib. Multilenguaje. Multiapariencia. Skins. Privilegios. Más
de 40 componentes integrados y personalizables. Múltiples problemas de
programación resueltos. Administración centralizada de recursos. Para
Delphi 3-6 y C++ Builder 3-5. http://www.jfactivesoft.com/spindex.htm
________________________________________________________________________
2. HILOS DE EJECUCION (THREADS)
Introducción
============
En Windows de 32 bits Ud. puede tener varios programas corriendo al
mismo tiempo. Por ejemplo, puede estar imprimiendo un documento y
simultáneamente revisar su disco duro en busca de virus con un programa
antivirus y mientras tanto, para matar el tiempo, puede jugar al
solitario, recibir su correo o realizar cualquier otra tarea.
Sin embargo, dado que el microprocesador (UCP) sólo es capaz de realizar
una sola operación en un momento dado, ¿cómo es posible ejecutar varios
programas al mismo tiempo? Bueno, primero, cuando un programa está
cargado en memoria para ser ejecutado se lo denomina "proceso", y
segundo, si su computadora tiene una sola UCP, entonces sólo un proceso
puede estar ejecutándose en un determinado momento.
¿Cómo es entonces que parece como si hubieran varios procesos ejecután-
dose simultáneamente? Esta es la pregunta correcta. "Parece como si"
varios procesos se ejecutaran al mismo tiempo gracias a las capacidades
multi-tarea de Windows.
¿Cómo funciona esta multitarea? Bueno, supongamos que tenemos cinco
procesos (A, B, C, D y E). El SO (Sistema Operativo) le asigna un
período muy corto de tiempo a un proceso (por ej. el A) y luego cuando
se acaba ese tiempo, el proceso es detenido temporalmente y el SO le da
un período muy corto de tiempo a otro proceso (por ej. el B), y así
sucesivamente con el resto de los procesos (por ej. el C, D y E), hasta
que vuelve a ser el turno entonces del primer proceso (el A, en nuestro
ejemplo), y así el ciclo se repite una y otra vez, de modo tal que en
definitiva los procesos se ejecutan un brevísimo lapso de tiempo, se
detienen (para permitir que los otros se ejecuten un brevísmio lapso de
tiempo también), se vuelven a ejecutar otro poco, se detienen, y así
sucesivamente hasta que terminan, dando la sensación de una ejecución
continua sin interrupciones, del mismo modo en que una película no es
más que una serie de fotogramas estáticos que cambian tan rápidamente
que percibimos la ilusión de un movimiento continuo. De esta forma,
parece como si varios procesos estuvieran corriendo al mismo tiempo.
Ahora bien, las capacidades multitarea de Windows no se limitan a
procesos, sino que dentro de un mismo proceso también es posible tener
partes del mismo ejecutándose "al mismo tiempo", es decir, en distintos
"hilos de ejecución" (threads). Normalmente usamos uno solo (el hilo
principal de la VCL), pero podemos crear y ejecutar otros hilos dentro
una aplicación. En este caso, se dice que la aplicación es multi-hilos.
¿Cuándo necesitaríamos crear otros hilos? Cuando queramos que nuestra
aplicación realice varias cosas al mismo tiempo, por supuesto, siendo el
caso típico cuando necesitamos realizar alguna tarea que pueda demorar
mucho (como realizar un gran apareo de archivos o buscar un fichero en
todo el disco duro, por citar algunos ejemplos), y deseamos que mientras
se realice esta tarea nuestra aplicación responda a eventos, de modo que
el usuario pueda detener prematuramente la tarea antes que ésta finalice
y/o que pueda hacer otras cosas con nuestra aplicación.
La clase TThread
================
La clase TThread (declarada en la unidad Classes) encapsula las llamadas
a la API de Windows necesarias para manejar distintos hilos de ejecu-
ción. Esta clase tiene un método abstracto (llamado Execute), y por ende
es una clase abstracta, por lo que no puede ser instanciada (es decir,
crear objetos de esa clase). Lo que se debe hacer es crear una clase
descendiente de TThread (por ejemplo TThread1) y redefinir (override) el
método Execute, que es el método que se ejecutará en un hilo de ejecu-
ción diferente.
Para realmente ejecutar este hilo, primero debemos declarar un objeto de
esta nueva clase (por ejemplo Thread1) y luego construirlo:
Thread1 := TThread1.Create(False);
El parámetro para el constructor es el valor que tendrá la propiedad
Suspended (suspendido). Si éste es False, se llamará a Execute inmedia-
tamente después que el objeto sea creado. Esta llamada se realizará de
modo asíncrono, es decir, el método Execute se ejecutará en un nuevo
hilo de ejecución, y el hilo principal de la VCL también seguirá ejecu-
tándose, es decir que la ejecución de la aplicación no se detiene a
esperar que Execute termine, sino que continúa ejecutándose "al mismo
tiempo".
Normalmente el hilo se crea "suspendido" (pasando True como parámetro en
la llamada al constructor), de modo que primero podamos establecer los
valores de otras propiedades del hilo de ejecución como Priority (prio-
ridad) y FreeOnTerminate (liberar al terminar) antes de asignar False a
Suspended para comenzar la ejecución del hilo (lo que hará que se llame
al método Execute asíncronamente).
Los hilos con mayor prioridad obtienen más tiempo de la UCP que los
otros y aunque eso hará que se ejecuten "más rápido", puede que
ralentice otros hilos de la aplicación, por lo que se debe ser crite-
rioso al establecer el valor de la propiedad Priority.
La propiedad FreeOnTerminate determina si el objeto se destruye una vez
que el método Execute finalice su ejecución (liberándonos de esta
tarea), y en tal caso, el método Destroy será llamado (y podemos utili-
zarlo por ejemplo para notificar al hilo principal que este hilo ha
terminado).
Para detener prematuramente la ejecución del hilo se debe poner en True
la propiedad Terminated. Es importante hacer notar que eso no termina el
hilo inmediatamente, sino que supuestamente el método Execute debería
estar constantemente chequeando el valor de la propiedad Terminated para
ver si se le requiere que finalice, y en tal caso limpiar (liberar
recursos, restablecer de variables, etc.) y salir del procedimiento.
Ejemplo de TThread
==================
En este ejemplo vamos a utilizar un hilo como una propiedad de un
formulario. El formulario tendrá un botón de inicio (para comenzar la
ejecución del hilo), un botón de detención (para cancelar la ejecución
del hilo) y una barra de progreso para visualizar el progreso de la
ejecución del hilo, que consistirá simplemente de un bucle "para" (for).
Para probar este ejemplo, primero cree una aplicación nueva. En el
formulario (Form1) añada una barra de progreso (ProgressBar, que debería
estar en la paleta Win32) y dos botones (Button, en la paleta Standard).
En el Inspector de Objetos, establezca las propiedades de esos objetos:
ProgressBar1
Max := 100000
Step := 1
Smooth := True
Button1
Caption := '&Iniciar'
Button2
Cancel := True
Caption := '&Detener'
Enabled := False
Al botón de detención lo hemos inhabilitado porque el hilo no estará
inicialmente en ejecución cuando se cree el formulario. Cuando lo
hagamos ejecutar (en el evento OnClick del botón de inicio) entonces
habilitaremos el botón de cancelación (para que el usuario pueda
detener el hilo) e inhabilitaremos el botón de inicio (porque el hilo ya
se habría iniciado). Cuando el hilo finalice su ejecución, deberíamos
volver a habilitar el botón de inicio (para que el usuario pueda volver
a ejecutar el hilo si así lo desea) e inhabilitar el botón de
cancelación.
Pero, ¿cómo sabremos que el hilo ha finalizado? Una forma de saber es
establecer la propiedad FreeOnTerminate en True antes de ejecutar el
hilo, de modo que cuando termine, el objeto que representa al hilo sea
destruido y en su método Destroy podamos enviar un mensaje al formulario
"dueño" del hilo y capturar este mensaje en el formulario.
Ahora vayamos al código. Antes de las declaraciones type --en donde está
declarado el formulario-- declare una constante para identificar el
mensaje que será enviado del hilo al formulario.
const
WM_ThreadDoneMsg = WM_User + 8;
WM_User es una constante predefinida que indica el valor mínimo de los
mensajes definidos por el usuario. Los valores por debajo de WM_User
están reservados para los mensajes de Windows. El ocho (8) que le
sumamos es un valor arbitrario.
Dentro de las declaraciones de tipos (type), antes de la declaración del
formulario, declare la clase del hilo como sigue:
TThread1 = class(TThread)
private
OwnerHandle: HWND;
ProgressBar1: TProgressBar;
procedure UpdateProgressBar;
protected
procedure Execute; override;
published
constructor Create(Owner: TForm; ProgressBar: TProgressBar);
destructor Destroy; override;
end;
Esto amerita algunas explicaciones:
1) Necesitamos el manejador (window handle) del formulario que contiene
el hilo para poder enviarle el mensaje WM_ThreadDoneMsg cuando el hilo
termine, por lo que declaramos una propiedad OwnerHandle que contenga
este manejador (de tipo HWND) y también redefinimos el método Destroy
(porque allí incluiremos el código para enviar el mensaje).
2) Debido a que usaremos la barra de progreso del formulario desde el
hilo, creamos la propiedad ProgressBar1 para tener una referencia a ese
componente. Otra posibilidad sería por ejemplo tener directamente una
referencia al formulario, pero debería ser de tipo type TForm1 (lo que
implica que hay que declarar la clase como forward) o de tipo TForm
(que implicaría que deberemos castar esa referencia como TForm1 antes
de acceder a la barra de progreso), así que consideramos la solución
adoptada como la más simple.
3) El método UpdateProgressBar será llamado desde Execute para actua-
lizar la barra de progreso (simplemente la hará avanzar). ¿Por qué no
avanzar la barra de progreso directamente desde el método Execute? Se
hace así para evitar conflictos multi-hilos (dos hilos accediendo la
misma memoria o recursos pueden potencialmente introducir errores en la
aplicación). Ese método UpdateProgressBar será llamado usando el método
Synchronize de la clase TThread, que hará que se ejecute en el hilo
principal de la VCL de modo que no conflictúe con él.
4) Redefinimos el método Execute. Una obligación, si queremos poder
instanciar la clase TThread1.
5) Declaramos nuestro propio constructor, que recibe como parámetros una
referencia al formulario propietario del hilo y otra a la barra de
progreso.
Una vez que hemos declarado la clase TThread1 class, en la sección
privada de la clase TForm1 agregue las siguientes declaraciones:
private
Thread1: TThread1;
procedure Thread1Done(var AMessage: TMessage);
message WM_ThreadDoneMsg;
Aquí, Thread1 representa al hilo y es un objeto de la clase TThread1
declarada más arriba, mientras que Thread1Done es una especie de evento
que se ejecutará cuando el formulario reciba el mensaje WM_ThreadDoneMsg.
Los métodos que responden a mensajes siempre reciben una referencia a un
objeto TMessage como parámetro. En nuestro ejemplo no lo usaremos, pues
la simple recepción del mensaje es todo lo que nos importa.
En el Inspector de Objetos genere los eventos OnClick de los dos botones
y el evento OnClose del formulario, insertando el código que sigue en
dichos manejadores de evento:
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Enabled := False; // Inhabilita el botón de inicio
Button2.Enabled := True; // Habilita el botón de detención
// Crea e inicia el nuevo hilo de ejecución
Thread1 := TThread1.Create(Self, ProgressBar1);
// La ejecución de la aplicación continúa aquí y "al mismo
// tiempo" se ejecuta el método Execute del objeto Thread1
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Thread1.Terminate; // Detiene el hilo. No ocurre inmediatamente.
end;
procedure TForm1.FormClose(Sender: TObject;
var Action: TCloseAction);
begin
if Button2.Enabled then begin // Si el hilo está ejecutándose
Thread1.Terminate; // Lo detiene y
Thread1.WaitFor; // espera a que termine
end;//if
Action := caFree; // Libera el formulario
// después que se cierre
end;
Ahora agregue el siguiente código en la sección Implementation de la
unidad.
procedure TForm1.Thread1Done(var AMessage: TMessage);
// Captura el mensaje enviado por el hilo señalando que ha
// terminado su ejecución
begin
Button1.Enabled := True; // Habilita el botón de inicio
Button2.Enabled := False; // Inhabilita el botón de detención
end;
constructor TThread1.Create(Owner: TForm;
ProgressBar: TProgressBar);
begin
inherited Create(True); // Crea el hilo suspendido
OwnerHandle := Owner.Handle; // Manejador del formulario
ProgressBar1 := ProgressBar; // Ref. a la barra de progreso
ProgressBar1.Position := 0; // Inicializa la barra de progreso
Priority := tpNormal; // Establece la prioridad del hilo
FreeOnTerminate := True; // El hilo se liberará solo
Suspended := False; // Inicia la ejecución del hilo
// ==> La ejecución de la aplicación continúa aquí y "al mismo
// tiempo" se ejecuta el método Execute (modo asíncrono)
end;
destructor TThread1.Destroy;
// Se llamará cuando Execute termine porque FreeOnTerminate fue
// puesto en True
begin
// Envía un mensaje al formulario para notificarle que el hilo
// ha terminado.
PostMessage(OwnerHandle, WM_ThreadDoneMsg, Self.ThreadID, 0);
inherited Destroy; // Llama al destructor del ancestro
end;
procedure TThread1.Execute; // Ejecución del hilo
var i: integer;
begin
for i := 1 to 100000 do begin
Synchronize(UpdateProgressBar); // Actualiza la barra de
// progreso en el hilo ppal.
if Terminated then break; // Finaliza el bucle si el
// hilo es detenido
end;//for
end;
procedure TThread1.UpdateProgressBar;
// Este método es llamado desde el método Execute en modo
// sincronizado para que se ejecute en el hilo principal de la
// VCL, evitando posibles conflictos multi-hilos.
begin
ProgressBar1.StepIt; // Hace avanzar la barra de progreso
end;
Bueno, ¡eso es todo! El código fuente completo de este ejemplo está
disponible en el archivo adjunto.
________________________________________________________________________
3. EJECUCION DE UNA APLICACION EXTERNA
Muchas veces necesitamos ejecutar otro programa desde el nuestro. Para
hacer esto, podemos utilizar la función ShellExecute declarada en la
unidad ShellAPI. La sintaxis es:
ShellExecute(Manejador, Operación, NombreFichero, Parámetros, Carpeta,
Mostrar)
Manejador (HWND) es el manejador (window handle) de la ventana madre,
por ejemplo el manejador del formulario principal de nuestra aplicación.
Operación (PChar) es un puntero a una cadena terminada en nulo conte-
niendo el nombre de la operación a realizar, pudiendo ser "open"
(abrir), "print" (imprimir) o "explore" (explorar carpeta). Este
parámetro puede ser Nil y en tal caso se asumirá la operación "open"
(abrir).
NombreFichero (PChar) es un puntero a una cadena terminada en nulo
conteniendo el camino y el nombre de la aplicación a ejecutar, el
documento abrir o imprimir con la aplicación asociada o la carpeta a
abrir o explorar.
Parámetros (PChar) es un puntero a una cadena terminada en nulo conte-
niendo los parámetros que se pasan a la aplicación indicada en
NombreFichero. Si NombreFichero no especificaba un ejecutable sino un
documento, entonces Parámetros debe ser Nil.
Carpeta (PChar) es un puntero a una cadena terminada en nulo conteniendo
el camino de la carpeta que se tomará como directorio por omisión de la
aplicación. Se corresponde con el cuadro de texto "Iniciar en:" de las
propiedades de los accesos directos. Este parámetro puede ser Nil.
Mostrar (Integer) especifica la forma en que se mostrará la aplicación
especificada en NombreFichero. Hay varios valores posibles:
SW_HIDE SW_SHOWMAXIMIZED
SW_MAXIMIZE SW_SHOWMINIMIZED
SW_MINIMIZE SW_SHOWMINNOACTIVE
SW_RESTORE SW_SHOWNA
SW_SHOW SW_SHOWNOACTIVATE
SW_SHOWDEFAULT SW_SHOWNORMAL
La documentación indica que si NombreFichero se refiere a un documento,
Mostrar debería ser 0, sin embargo si prueban otros valores verán que sí
funcionan.
Valor devuelto:
Si la función tiene éxito, ShellExecute devuelve un valor tipo HINST
(definido como LongWord) con el manejador (handle) de la aplicación que
se ejecutó o el manejador de la aplicación servidora DDE. Si la función
fracasa (o sea, si la aplicación no pudo iniciarse), el valor devuelto
es un código de error entre 0 y 32.
Veamos ahora algunos ejemplos:
if ShellExecute(Form1.Handle, nil, 'c:\windows\general.txt',
nil, nil, SW_SHOWNORMAL) <= 32 then
Application.MessageBox('No se pudo ejecutar la aplicación',
'Error', MB_ICONEXCLAMATION);
ShellExecute(Form1.Handle, nil, 'c:\windows\notepad.exe',
'c:\windows\general.txt', nil, SW_SHOWMAXIMIZE);
ShellExecute(Form1.Handle, 'open', 'c:\windows\notepad.exe',
'general.txt', 'c:\windows', SW_SHOWNORMAL);
ShellExecute(Form1.Handle, nil, PChar(fname + '.txt'), nil,
nil, SW_MAXIMIZE);
ShellExecute(Form1.Handle, nil, 'c:\windows\notepad.exe',
nil, nil, SW_SHOWNORMAL);
Nota:
Las aplicaciones llamadas por ShellExecute se ejecutan en modo
asíncrono, es decir, la ejecución de nuestra aplicación continúa sin
esperar a que la aplicación llamada por ShellExecute finalice. Si
deseamos que un proceso se ejecute en modo síncrono, no debemos usar
ShellExecute sino un código como el siguiente:
procedure TForm1.Button1Click(Sender: TObject);
var
proc_info: TProcessInformation;
startinfo: TStartupInfo;
ExitCode: longword;
begin
// Inicializar las estructuras
FillChar(proc_info, sizeof(TProcessInformation), 0);
FillChar(startinfo, sizeof(TStartupInfo), 0);
startinfo.cb := sizeof(TStartupInfo);
// Intenta crear el proceso
if CreateProcess('c:\windows\notepad.exe', nil, nil, nil,
false, NORMAL_PRIORITY_CLASS, nil, nil, startinfo,
proc_info) <> False then begin
// El proceso se creó con éxito
// Ahora esperar hasta que el proceso termine...
WaitForSingleObject(proc_info.hProcess, INFINITE);
// El proceso ha terminado. Ahora lo cerramos.
GetExitCodeProcess(proc_info.hProcess, ExitCode); // Opcional
CloseHandle(proc_info.hThread);
CloseHandle(proc_info.hProcess);
Application.MessageBox((PChar(Format(
'¡Bloc de notas finalizado! (Código de retorno=%d)', [ExitCode])),
'Aviso', MB_ICONINFORMATION);
end else begin
// No se puedo crear el proceso
Application.MessageBox('No se pudo ejecutar la aplicación',
'Error', MB_ICONEXCLAMATION);
end;//if
end;
Para abrir un fichero con la aplicación, emplee un código como el que se
muestra a continuación:
if CreateProcess(nil,
'"c:\windows\notepad.exe" "c:\windows\general.txt"', ...
Las comillas dobles sólo son necesarias cuando el camino o el nombre de
la aplicación o del documento contienen espacios.
La función CreateProcess sólo funciona con achivos ejecutables, es
decir, no ejecuta documentos con la aplicación asociada. La solución a
esta cuestión la veremos en el próximo número.
________________________________________________________________________
4. ¿QUE SIGUE?
En el próximo número trataremos de responder todas las preguntas de
nuestros lectores respecto de los temas tratados en éste, y veremos cómo
hacer algo más útil en el método Execute, por ejemplo que revise
ficheros en una carpeta en búsqueda de una palabra clave y también
veremos un ejemplo de uso del Registro de Windows para abrir un
documento con su aplicación asociada y esperar a que ésta termine.
¡Nos vemos!
________________________________________________________________________
Si no has recibido el archivo con el código fuente completo de los
ejemplos que se presentan en este boletín, puedes descargarlo de la
siguiente dirección: http://www.latiumsoftware.com/es/file.php?id=d01
________________________________________________________________________
Página principal: http://www.latiumsoftware.com/es/pascal/index.php
Página del grupo: http://espanol.groups.yahoo.com/group/boletin-pascal/
Para suscribirse / apuntarse: boletin-pascal-subscribe@gruposyahoo.com
Para cancelar / removerse: boletin-pascal-unsubscribe@gruposyahoo.com
Para reportar problemas con la suscripción: eds2008 @ latiumsoftware.com
________________________________________________________________________
Este boletín se provee "TAL Y COMO ESTA", sin garantía de ninguna clase.
Su uso implica la aceptación de nuestros términos de licencia y de la
ausencia de garantía que puedes leer en nuestro sitio web. Allí también
encontrarás una nota sobre marcas registradas. Te animamos a que redis-
tribuyas este boletín, siempre y cuando lo hagas en forma completa
(incluyendo la información de copyright), sin modificaciones y de manera
gratuita. Los artículos son copyright de sus respectivos autores y se
reproducen aquí con el permiso de los mismos.
________________________________________________________________________
Latium Software http://www.latiumsoftware.com/es/index.php
Copyright (c) 2000 por Ernesto De Spirito. Todos los derechos reservados
________________________________________________________________________
|