Mostrando entradas con la etiqueta sistema. Mostrar todas las entradas
Mostrando entradas con la etiqueta sistema. Mostrar todas las entradas

17 julio 2009

El componente BackwroundWorker (y 2)

Vamos a ver un ejemplo de cómo utilizar este componente con un hilo de ejecución que recorra todos los números del 1 al 100 y nos calcule los múltiplos de 5, 7 y 9. Este sería el formulario principal de nuestro proyecto:


El formulario se compone de los siguientes componentes:

- 2 botones de la clase TButton llamados BComenzar y BCancelar para iniciar o detener el hilo de ejecución.

- 1 componente TBackgroundWorker llamado BackgroundWorker.

- 1 barra de progreso de la clase TProgressBar llamada Progreso y con su propiedad Smooth a True.

- 3 etiquetas (TLabel) y 3 casillas (TEdit) llamadas Multiplos5, Multiplos7 y Multiplos9 donde se irán alojando los múltiplos encontrados de cada uno.

- 1 barra de estado de la clase TStatusBar llamada Estado con su propiedad SimplePanel a True.

Comencemos a meter código:

1º Al pulsar el botón Comenzar ponemos en marcha el hilo:

procedure TForm1.BComenzarClick(Sender: TObject);
begin
BackgroundWorker.Execute;
end;

2º Al pulsar el botón Cancelar le pedimos al hilo que se detenga:

procedure TForm1.BDetenerClick(Sender: TObject);
begin
BackgroundWorker.Cancel;
end;

3º En el evento OnWork del componente BackgroundWorker recorremos los 100 números:

procedure TForm1.BackgroundWorkerWork(Worker: TBackgroundWorker);
var
i: Integer;
begin
for i := 1 to 100 do
begin
// ¿Hay que cancelar el proceso?
if Worker.CancellationPending then
begin
// Le indicamos al hilo que lo cancelamos
Worker.AcceptCancellation;
Exit;
end;

// ¿Es múltiplo de 5?
if i mod 5 = 0 then
Worker.ReportFeedback(5, i);

// ¿Es múltiplo de 7?
if i mod 7 = 0 then
Worker.ReportFeedback(7, i);

// ¿Es múltiplo de 9?
if i mod 9 = 0 then
Worker.ReportFeedback(9, i);

// Esperamos 50 milisegundos
Sleep(50);

// Incrementamos la barra de progreso
Worker.ReportProgress(i);
end;
end;

Al principio del bucle controlamos si nos piden abandonar el hilo de ejecución:

// ¿Hay que cancelar el proceso?
if Worker.CancellationPending then
begin
// Le indicamos al hilo que lo cancelamos
Worker.AcceptCancellation;
Exit;
end;

Después comprobamos los múltiplos de cada número y en el caso de que así sea envío provoco un evento OnWorkFeedBack para notificar el número que he encontrado:

// ¿Es múltiplo de 5?
if i mod 5 = 0 then
Worker.ReportFeedback(5, i);

// ¿Es múltiplo de 7?
if i mod 7 = 0 then
Worker.ReportFeedback(7, i);

// ¿Es múltiplo de 9?
if i mod 9 = 0 then
Worker.ReportFeedback(9, i);

Y por último provoco un pequeño retardo de 50 milisegundos (para ir viendo el progreso) y provoco un evento OnWorkProgress para notificar el incremento en la barra de progreso:

// Esperamos 50 milisegundos
Sleep(50);

// Incrementamos la barra de progreso
Worker.ReportProgress(i);

Sigamos...

4º En el evento OnWorkFeedBack recogemos el mensaje que nos manda el hilo para ir guardando en cada casilla los múltiplos de cada número:

procedure TForm1.BackgroundWorkerWorkFeedback(Worker: TBackgroundWorker;
FeedbackID, FeedbackValue: Integer);
begin
case FeedbackID of
5: Multiplos5.Text := Multiplos5.Text + IntToStr(FeedbackValue) + ',';
7: Multiplos7.Text := Multiplos7.Text + IntToStr(FeedbackValue) + ',';
9: Multiplos9.Text := Multiplos9.Text + IntToStr(FeedbackValue) + ',';
end;
end;

5º En el evento OnWorkProgress actualizamos la barra de progreso y la barra de estado en pantalla:

procedure TForm1.BackgroundWorkerWorkProgress(Worker: TBackgroundWorker;
PercentDone: Integer);
begin
Progreso.Position := PercentDone;
Estado.SimpleText := 'Procesando... ' + IntToStr(PercentDone) + '%';
end;

6º En el evento OnWorkComplete le decimos al usuario en la barra de progreso que hemos terminado:

procedure TForm1.BackgroundWorkerWorkComplete(Worker: TBackgroundWorker;
Cancelled: Boolean);
begin
Estado.SimpleText := 'Proceso finalizado';
end;

7º Por si acaso al usuario le da por cerrar el formulario mientras está el hilo en marcha tenemos que cancelarlo y esperar a que termine (al igual que hacen los programas como Emule o BitTorrent cuando lo cerramos y espera a liberar memoria y cerrar las conexiones):

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// ¿Esta el hilo en marcha?
if BackgroundWorker.IsWorking then
begin
// Le indicamos que se detenga
BackgroundWorker.Cancel;
// Esperamos a que se detenga
BackgroundWorker.WaitFor;
end;
end;

Al ejecutar el programa irá rellenando las casillas correspondientes:


Hasta finalizar el proceso:


Como podemos apreciar, este componente nos facilita mucho la labor con los hilos de ejecución respecto a la clase TThread. Por las pruebas que he realizado en otros programas más complejos (facturación) es bastante sencillo de manejar y muy estable.

Pruebas realizadas en RAD Studio 2007.

03 julio 2009

El componente BackgroundWorker (1)

Aunque haya finalizado el tema referente a los hilos de ejecución con la clase TThread no está demás hablar de otros componentes no oficiales de Delphi que también están relacionados con los hijos de ejecución.

En este caso el componente me lo recomendó un lector de este blog (gracias a Jorge Abel) y me puse a echarle un vistazo a ver que tal. Este componente puede descargarse gratuitamente de esta página web (licencia Freeware):

http://www.delphiarea.com/products/

Concretamente en la sección de componentes que viene más abajo:


Al igual que la clase TThread, este componente permite crear un hilo de ejecución paralelo al hilo primario de nuestra aplicación pero de una manera sencilla y sin tener que heredad de clases.

INSTALAR EL COMPONENTE

Como es un componente que va suelto (sin paquete) vamos a proceder a crear un paquete para el sólo. Para ello creamos una carpeta llamada DephiArea donde vamos a descomprimir los archivos del componente:


Ahora nos vamos a Delphi y seleccionamos File -> New -> Package – Delphi for Win32. Aparecerá esto a la derecha en la ventana del proyecto:


Ahora seleccionamos File -> Save all y nos vamos a la carpeta donde hemos descomprimido el componente, por ejemplo:

D:\CodeGear\RAD Studio\5.0\Componentes\DelphiArea\

Y guardamos el proyecto con el nombre DelphiArea.dproj. Volvemos a la ventana del proyecto y pinchamos el nombre del mismo con el botón derecho del ratón y seleccionamos Add:


En esta ventana que aparece pulsamos el botón Browse y elegimos el archivo BackgroundWorker.pas:


Ahora ya sólo tenemos que seleccionar Compile:


Y después Install:


Si todo ha ido bien mostrará este mensaje:


Si creamos un nuevo proyecto y nos fijamos en la paleta de componentes veremos el nuevo componente que hemos instalado:


Ya estamos listos para comenzar a trabajar con el mismo.

LAS CARACTERÍSTICAS DEL COMPONENTE BACKGROUNDWORKER

Insertamos el componente en el formulario donde queremos lanzar el hilo de ejecución:


Si nos fijamos en el inspector de objetos veremos que no se han estresado añadiendo propiedades:


Aunque mediante código tenemos acceso a estas variables:

CancellationPending: esta bandera booleana es utilizada para indicarnos que debemos abandonar el bucle principal del hilo (el evento OnWork que veremos más adelante). Por defecto está a False. Se pondrá a True cuando llamemos al método Cancel.

IsCancelled: variable booleana que nos dice si el hilo ha sido cancelado.

IsWorking: indica si el hilo se sigue ejecutando con normalidad.

ThreadID: es un número entero con el identificador que Windows ha asignado a nuestro hilo. Podemos utilizar esta propiedad para llamar a otras funciones de la API de Windows relacionadas con los hilos y procesos.

Y para modificar su comportamiento tenemos estos métodos:

procedure Execute;

Comienza la ejecución del hilo.

procedure Cancel;

Solicita al hilo que detenga la ejecución, es decir, pone la variable CancellationPending a True para que seamos nosotros los que abandonemos el bucle cerrado del evento OnWork.

procedure WaitFor;

Espera a que termine la ejecución del hilo.

procedure ReportProgress(PercentDone: Integer);

A este método sólo podemos llamarlo dentro del evento OnWork. Lo que hace es realizar una llamada al evento OnWorkProgress para que situemos en el mismo lo que vamos a hacer para informar al hilo primario del porcentaje de trabajo que hemos realizado.

procedure ReportProgressWait(PercentDone: Integer);

Igual que el procedimiento anterior pero espera a que termine el código que hemos colocado dentro de OnWorkProgress.

procedure ReportFeedback(FeedbackID, FeedbackValue: Integer);

Al igual que los dos procedimientos anteriores, sólo podemos llamar al mismo dentro del evento OnWork. Realiza una llamada al evento OnWorkFeedBack para que podamos enviar mensajes al hilo primario.

procedure ReportFeedbackWait(FeedbackID, FeedbackValue: Integer);

Lo mismo que el procedimiento anterior pero espera a que termine la ejecución del código que hemos puesto en el evento OnWorkFeedBack.

procedure Synchronice(Method: ThreadMethod);

Sólo podemos llamar a este procedimiento dentro del evento OnWork. Le pasamos como parámetro el método del hilo principal (VCL) para que lo ejecute por nosotros, por ejemplo, para mostrar información en pantalla o incrementar una barra de progreso.

procedure AcceptCancellation;

Sólo podemos llamar a este procedimiento dentro del evento OnWork. Debemos llamar a este procedimiento cuando dentro del evento OnWork hemos visto que la variable CancellationPending es True y entonces abandonamos el bucle principal y llamamos a este procedimiento para informar a hilo que hemos aceptado salir del mismo.

Estos son los eventos que incorpora:


Veamos para que sirve cada evento:

OnWork: Este método equivale al procedimiento Execute de la clase TThread, de hecho, se ejecuta cuando llamamos al procedimiento Execute. Es aquí donde debemos introducir el bucle continuo que va a hacer el trabajo pesado. A cada ciclo del bucle debemos comprobar el valor de la variable CancellationPending para saber si debemos terminarlo. Una vez abandonemos el bucle debemos llamar al método AcceptCancellation que pondrá la variable IsCancelled a True.

OnWorkComplete: este evento se ejecutará cuando termine la ejecución del código que hemos puesto en el evento OnWork ya sea porque ha terminado su ejecución o porque lo hemos cancelado. Aquí podemos poner el código encargado de liberar los objetos creados o cerrar archivos abiertos.

OnWorkProgress: se ejecutará el código que introducimos a este evento cuando desde el evento OnWork hemos hecho una llamada a los procedimientos ReportProgress o ReportProgressWait. Desde aquí podemos informar al hilo primario de sobre cómo vamos.

OnWorkFeedBack: se ejecuta cuando nuestro hilo envía mensajes al hilo primario mediante los procedimientos ReportFeedBack o ReportFeedBackWait.

COMO INSTALARLO EN DELPHI 7

Para instalar este componente en Delphi 7 hay que seguir estos pasos:

1º Seleccionamos en el menú superior Install -> Component.

2º En la ventana que aparece seleccionamos la pestaña Into new Package.

3º Pulsamos el botón Browse del campo Unit file name y seleccionamos el archivo BackgroundWorker.pas.

4º Pulsamos el botón Browse del campo Package file name y escribimos DelphiArea.dpk.

5º En el campo Package Description escribimos Delphi Area.

Suponiendo que el componente lo tengo descomprimido en esta carpeta:

D:\Borland\Delphi7\Componentes\BackgroundWorker\

Quedaría la ventana de este modo:


Pulsamos el botón Ok y aparecerá esta mensaje:


Pulsamos el botón Yes y si todo ha ido bien aparecerá este mensaje:


Con esto ya tiene que aparecer arriba en la paleta de componentes:


En el próximo artículo veremos un ejemplo de cómo crear un hilo de ejecución utilizando este componente.

Pruebas realizadas en RAD Studio 2007.

19 junio 2009

Los Hilos de Ejecución (y 4)

Hoy voy a terminar de hablar de otras cuestiones relacionadas con los hilos de ejecución como pueden ser las variables de tipo threadvar y las secciones críticas.

ACCESO A LA MISMA VARIABLE POR MULTIPLES HILOS

Antes de ver como utilizar una variable threadvar veamos un problema que se puede plantear cuando varios hilos intentan acceder a una variable global sin utilizar el método synchronize.

Siguiendo con nuestro ejemplo de los tres hilos que incrementan una barra de progreso cada uno, supongamos que quiero que cada barra llegue de 0 a 100 y que cuando termine voy a hacer que termine el hilo.

Después voy a crear una variable global llamada iContador:

var
iContador: Integer;

Cuando un hilo incremente la barra de progreso entonces incrementará también esta variable global iContador:

procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Inc(iContador);
Synchronize(ActualizarProgreso);
Sleep(100);
end;
end;

También he añadido al formulario una etiqueta llamada ETotal que mostrará por pantalla el contenido de la variable iContador:


El procedimiento de ActualizarProgreso incrementará la barra de progreso y mostrará el contador del formulario:

procedure THilo.ActualizarProgreso;
begin
Progreso.StepIt;
FTresHilos.ETotal.Caption := IntToStr(iContador);

if Progreso.Position = 100 then
Terminate;
end;

Supuestamente, si tres hilos de ejecución incrementan cada barra de 0 a 100 entonces cuando terminen de ejecutarse el contador tendrá el valor 300. Pero no es así:


Me ha salido 277 pero lo mismo puede dar 289 que 291. Como cada hilo accede a la variable global iContador simultáneamente lo mismo la incrementa después de otro hilo que machaca el incremento del hilo anterior.

Ya vimos que esto puede solucionarse incluyendo la sentencia Inc(iContador) dentro del procedimiento ActualizarProgreso, de modo que mediante la sentencia Synchronize sólo el hilo primario podrá incrementar esta variable. Esto tiene un inconveniente y es que se forma un cuello de botella en los hilos de ejecución porque cada hilo tiene que esperar a que el hilo primario incremente la variable.

Veamos si se puede solucionar mediante variables threadvar.

LAS VARIABLES THREADVAR

Las variables de tipo threadvar son declaradas globalmente en nuestro programa para que puedan ser leídas por uno o más hilos simultáneamente pero no pueden ser modificadas por los mismos. Me explico.

Cuando un hilo lee de una variable global threadvar, si intenta modificarla sólo modificará una copia de la misma, no la original, ya que solo puede ser modificada por el hilo primario. Delphi creará automáticamente una copia de la variable threadvar para cada hilo (como si fuera una variable privada dentro del objeto que hereda de Thread).

Una variable threadvar se declara igual que una variable global:

implementation

threadvar
iContador: Integer;

Si volvemos a ejecutar el programa veremos que iContador nunca de mueve:


Entonces, ¿de que nos sirve la variable threadvar? Su cometido es crear una variable donde sólo el hilo primario la pueda incrementar pero que a la hora de ser leía por un hilo secundario siempre tenga el mismo valor.

Para solucionar este problema lo mejor es que cada clase tenga su propio contador y que luego en el formulario principal creemos un temporizador que muestre la suma de los contadores de cada hilo y de este modo no se crean cuellos de botella ni es necesario utilizar synchronize.

Una utilidad que se le puede dar a este tipo de variables es cuando el hilo primario debe suministrar información crítica en tiempo real a los hijos secundarios y sobre todo cuando queremos que ningún hilo lea un valor distinto de otro por el simple hecho que lo ha ejecutado después.

SECCIONES CRÍTICAS

Anteriormente vimos como los objetos mutex podían controlar que cuando se ejecute cierto código dentro de un hilo de ejecución, los demás hilos tienen que esperarse a que termine.

Esto también puede crearse mediante secciones críticas (CrititalSection). Una sección crítica puede ser útil para evitar que varios hilos intenten enviar o recibir simultáneamente información de un dispositivo (monotarea). Naturalmente esto solo tiene sentido si hay dos o más instancias del mismo hilo, sino es absurdo.

Aprovechando el caso anterior de los tres hilos incrementando la barra de progreso, imaginemos que cada vez que un hilo intenta incrementa su barra de progreso, tiene que enviar su progreso por un puerto serie a un dispositivo. Aquí vamos a suponer que ese puerto serie a un objeto TStringList (que bien podía ser por ejemplo un TFileStream).

Primero creamos una variable global llamada Lista:

var
Lista: TStringList;

Después la creo cuando pulso el botón Comenzar:

procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Lista := TStringList.Create;
Progreso1.Position := 0;
Progreso2.Position := 0;
Progreso3.Position := 0;
...

Y en el procedimiento Execute envío la posición de su barra de progreso al StringList:

procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(ActualizarProgreso);
Lista.Add(IntToStr(Progreso.Position));
Sleep(100);
end;
end;

Pero al ejecutar el programa puede ocurrir esto:


Eso ocurre porque los tres hilos intentan acceder a la vez al mismo objeto StringList. Si bien podíamos solucionar esto utilizando Synchronize volvemos al problema de que cada hilo pierde su independencia respecto al hilo principal.

Lo que vamos a hacer es que si un hilo está enviando algo al objeto StringList los otros hilos no pueden enviarlo, pero si seguir su normal ejecución. Esto se soluciona creando lo que se llama una sección crítica que aísla el momento en que un hilo hace esto:

Lista.Add(IntToStr(Progreso.Position));

Para crear una sección crítica primero tenemos que declarar esta variable global:

var
Lista: TStringList;
SeccionCritica: TRTLCriticalSection;

Al pulsar el botón Comenzar inicializamos la sección crítica:

procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Lista := TStringList.Create;
InitializeCriticalSection(SeccionCritica);
Progreso1.Position := 0;
Progreso2.Position := 0;
Progreso3.Position := 0;
...

Acordándonos que hay que liberarla al pulsar el botón Detener:

procedure TFTresHilos.BDetenerClick(Sender: TObject);
begin
Hilo1.Terminate;
Hilo2.Terminate;
Hilo3.Terminate;
DeleteCriticalSection(SeccionCritica);
end;

Después hay que modificar el procedimiento Execute para introducir nuestra instrucción peligrosa dentro de la sección crítica:

procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(ActualizarProgreso);
EnterCriticalSection(SeccionCritica);
Lista.Add(IntToStr(Progreso.Position));
LeaveCriticalSection(SeccionCritica);
Sleep(100);
end;
end;

Todo lo que se ejecute dentro de EnterCritialSection y LeaveCriticalSection solo será ejecutado a la vez por hilo, evitando así la concurrencia. Con esto solucionamos el problema sin tener que recurrir al hilo primario.

Aunque he abarcando bastantes temas respecto a los hilos de ejecución todavía quedan muchas cosas que entran en la zona de la API de Windows y que se salen de los objetivos de este artículo. Si encuentro algo más interesante lo publicaré en artículos independientes.

Pruebas realizadas en RAD Studio 2007.

05 junio 2009

Los Hilos de Ejecución (3)

En la tercera parte de este artículo vamos a ver como establecer la prioridad en los hilos de ejecución así como controlar su comportamiento mediante objetos Event y Mutex.

LA PRIORIDAD EN LA EJECUCIÓN DE LOS HILOS

Aparte de poder controlar nosotros mismos la velocidad de hilo dentro del procedimiento Execute utilizando la función Sleep, GetTickCount o TimeGetTime también podemos establecer la prioridad que Windows le va a dar a nuestro hilo respecto al resto de aplicaciones.

Esto se hace utilizando la propiedad Priority que puede contener estos valores:

type
TThreadPriority = (
tpIdle, // El hilo sólo se ejecuta cuando el procesador está desocupado
tpLowest, // Prioridad más baja
tpLower, // Prioridad baja.
tpNormal, // Prioridad normal
tpHigher, // Prioridad alta
tpHighest, // Prioridad muy alta
tpTimeCritical // Obliga a ejecutarlo en tiempo real
);

No deberíamos abusar de las prioridades tpHigher, tpHighest y tpTimeCritical a menos que sea estrictamente necesario, ya que podían restar velocidad a otras tareas críticas de Windows (como nuestro querido Emule).

Vamos a ver un ejemplo en el que voy a crear tres hilos de ejecución que actualizarán cada uno una barra de progreso cada 100 milisegundos:


Esta sería la definición del hilo:

THilo = class(TThread)
Progreso: TProgressBar;
procedure Execute; override;
procedure ActualizarProgreso;
end;

Y su implementación:

{ THilo }

procedure THilo.ActualizarProgreso;
begin
Progreso.StepIt;
end;

procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(ActualizarProgreso);
Sleep(100);
end;
end;

Su única misión es incrementar la barra de progreso que le toca y esperar 100 milisegundos. Al pulsar el botón Comenzar hacemos esto:

procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Progreso1.Position := 0;
Progreso2.Position := 0;
Progreso3.Position := 0;
Hilo1 := THilo.Create(True);
Hilo2 := THilo.Create(True);
Hilo3 := THilo.Create(True);
Hilo1.Progreso := Progreso1;
Hilo2.Progreso := Progreso2;
Hilo3.Progreso := Progreso3;
Hilo1.Resume;
Hilo2.Resume;
Hilo3.Resume;
end;

Al ejecutarlo los tres hilos van exactamente iguales:


Pero ahora supongamos que hago esto antes de ejecutarlos:

Hilo1.Priority := tpTimeCritical;
Hilo2.Priority := tpLowest;
Hilo3.Priority := tpHighest;

Al ejecutarlo nos ponemos a navegar con Firefox o nuestro navegador preferido en páginas que den caña al procesador y al observar nuestros hilos veremos que se han desincronizado según su prioridad:


No es que sea una diferencia muy significativa pero a largo plazo se nota como Windows para repartiendo el trabajo. Esto puede venir muy bien para llamar a rutinas que procesen datos en segundo plano (tpIdle) o bien enviar comandos a máquinas conectadas al PC por puerto serie, paralelo o USB que necesiten ir como reloj suizo (tpTimeCritical).

CONTROLANDO LOS HILOS CON EVENTOS DE WINDOWS

Los hilos de ejecución también permiten comenzar su ejecución o detenerse dependiendo de un evento externo que o bien puede ser controlado por el hilo primario de nuestra aplicación o bien por otro hilo.

Los eventos son objetos definidos en la API de Windows que permiten tener dos estados: señalizados y no señalizados.

Para crear un evento se utiliza esta función de la API de Windows:

CreateEvent(
lpEventAttributes, // atributos de seguridad
bManualReset, // si está a True significa que nosotros nos encargamos de señalizarlo
bInitialState, // Señalizado o no señalizado
lpName // Nombre el evento
): THandle;

Los atributos de seguridad vienen definidos en la API de Windows (en C) del siguiente modo:

typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;

Si le ponemos nil le asignará los atributos por defecto que tenemos como usuario en el sistema operativo Windows. Este parámetro sólo sirve para los sistemas operativos Windows NT, XP y Vista. En los Windows 9X no hará nada.

¿Para que podemos utilizar los eventos? Pues podemos crear uno que le diga a los hilos cuando deben comenzar, detenerse o esperar cierto tiempo. Hay que procurar que el nombre del evento (lpName) sea original para que no colisione con otro nombre de otra aplicación. Para averiguar si un evento ha producido un error podemos llamar a la función:

function GetLastError: DWord;

Si devuelve un cero es que todo ha ido bien. Los objetos Event también pueden ser creados sin nombre, enviando como último parámetro el valor nil.

Para crear un evento voy a crear una variable global en la implementación con el Handle del evento que vamos a crear:

implementation

var
Evento: THandle;

Antes de crear los hilos y ejecutarlos creo el evento señalizado:

procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Evento := CreateEvent(nil, True, True, 'MiEvento');
...

Entonces modifico el procedimiento Execute para que haga cada ciclo si el evento está señalizado y si no es así, que espere indefinidamente:

procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(ActualizarProgreso);
Sleep(100);
WaitForSingleObject(Evento, INFINITE);
end;
end;

La función WaitForSingleObject espera a que el evento que le pasamos como primer parámetro esté señalizado para seguir, en caso contrario seguirá esperando indefinidadamente (INFINITE). También podíamos haber hecho que esperara un segundo:

WaitForSingleObject(Evento, 1000);

Para señalizar o no los eventos he añadido al formulario dos componentes RadioButton:


Para señalizarlo llamo a la función SetEvent:

procedure TFTresHilos.SenalizadoClick(Sender: TObject);
begin
SetEvent(Evento);
end;

Y para no señalizarlo utilizo PulseEvent:

procedure TFTresHilos.NoSenalizadoClick(Sender: TObject);
begin
PulseEvent(Evento);
end;

Tenemos dos funciones para no señalizar un evento:

PulseEvent: no señaliza el evento inmediatamente.

ResetEvent: no señaliza el evento la próxima vez que pase por WaitForSingleObject.

Y lo mejor de todo esto que no sólo podemos activar y desactivar hilos de ejecución dentro de la misma aplicación sino que además podemos hacerlo entre distintas aplicaciones que se están ejecutando a la vez.

En mi ejemplo he abierto dos instancias de la misma aplicación y no he señalizado en la primera un evento, con lo que se han detenido las dos:


Esto nos da mucha potencia para sincronizar distintas aplicaciones simultáneamente.

LOS OBJETOS MUTEX

La API de Windows también nos trae otro tipo de objetos llamados Mutex, también conocidos como semáforos binarios que funcionan de manera similar a los eventos pero que además permite asignarles un dueño. La misión de los objetos Mutex es evitar que varios hilos accedan a la vez a los mismos recursos, al estilo del procedimiento Synchronize.

Al contrario de los objetos Event donde todos los hilos podían esperar o no al evento en cuestión, con los objetos Mutex podemos hacer que solo uno de los hilos pueda trabajar a la vez y que los otros se esperen hasta nuevo aviso. Lo veremos más claro con un ejemplo.

Para crear un objeto Mutex utilizamos esta función:

function CreateMutex(lpMutexAttributes, bInitialOwer, lpName);

Al igual que con los eventos, el parámetro lpMutexAttributes sirve para establecer los atributos de seguridad. El parámetro bInitialOwer que se encarga de decir si el hilo que llama a CreateMutex es el propietario de este Mutex o por el contrario si queremos que el primer hilo que espere al Mutex es el dueño del mismo.

En el ejemplo que vimos anteriormente con las tres barras de progreso, supongamos que cada vez que se incrementa la barra no queremos que las otras barran lo hagan también. Esto puede ser muy útil cuando hay varios hilos que intentan acceder al mismo recurso (grabar en CD-ROM, enviar señales por un puerto, etc.).

Al contrario del ejemplo de los eventos que me interesaba que se activaran o desactivaran todos a la vez, aquí con los Mutex me interesa que mientras a un hilo le toca trabajar, los otros hilos tienen que esperarse a que termine (100 ms).

Al igual que hice con el objeto Event voy a crear una variable global con el handle del Mutex:

implementation

var
Mutex: THandle;

Ahora creo el Mutex al pulsar el botón Comenzar:

procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Mutex := CreateMutex(nil, True, 'Mutex1');
Progreso1.Position := 0;
Progreso2.Position := 0;
Progreso3.Position := 0;
Hilo1 := THilo.Create(True);
Hilo2 := THilo.Create(True);
Hilo3 := THilo.Create(True);
Hilo1.Progreso := Progreso1;
Hilo2.Progreso := Progreso2;
Hilo3.Progreso := Progreso3;
Hilo1.Resume;
Hilo2.Resume;
Hilo3.Resume;
ReleaseMutex(Mutex);
end;

Para que un hilo no se lance antes que otro cuando ejecuto Resume, lo que hago es que no suelto el Mutex hasta que los tres hilos están en ejecución (como en una carrera).

Después solo tengo que ir ejecutando cada uno haciendo esperar a los demás hasta que termine:

procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
WaitForSingleObject(Mutex, INFINITE); // captura el mutex (los demás a esperar)
Synchronize(ActualizarProgreso);
Sleep(100);
ReleaseMutex(Mutex); // mutex liberado hasta que lo capture otro hilo
end;
end;

Entre las líneas WaitForSingleObject y ReleaseMutex, lo demás hilos quedan parados hasta que termine. De este modo creamos un cuello de botella que impide que varios hilos accedan simultáneamente a los mismos recursos.

Al ejecutarlo este es el resultado:


En la foto no se aprecia pero cuando se ve en movimiento vemos que van escalonados, de trozo en trozo en vez de píxel a píxel.

También podíamos haber creado dos objetos Mutex que hagan que un hilo no comience a ejecutarse hasta que otro hilo le active su Mutex correspondiente. Las combinaciones que se pueden hacer con Mutex y Event son infinitas.

En el próximo artículo veremos como crear variables de tipo ThreadVar y otros asuntos interesantes.

Pruebas realizadas en RAD Studio 2007.

22 mayo 2009

Los Hilos de Ejecución (2)

En el artículo anterior vimos un sencillo ejemplo de cómo crear un contador utilizando un hilo de ejecución, pero hay un pequeño inconveniente que tenemos que arreglar.

Dentro de un hilo de ejecución podemos actualizar la pantalla (los formularios y componentes) siempre y cuando la actualización sea muy esporádica y el trabajo a realizar sea muy rápido (entrar y salir). Pero si durante el evento Execute de un hilo intentamos dibujar o actualizar los componentes VCL más de lo normal, puede provocar una excepción de este tipo:


También suele ocurrir si dos hilos ejecutándose en paralelo intentan actualizar el mismo componente VCL.

Esto se debe a que la librería VCL no ha sido diseñada para trabajar con problemas de concurrencia en múltiples hilos de ejecución.

SINCRONIZANDO CÓDIGO ENTRE HILOS

Para evitar este problema vamos a utilizar el procedimiento Synchronice que permite que un hilo llame a un procedimiento definido dentro del mismo pero en el contexto del hilo primario, es decir, el hilo principal de nuestro programa ejecuta el procedimiento que el hilo le manda como parámetro utilizando Synchronice. Es como si el hilo le dijera al hilo principal: ejecútame esto que a mi me da la risa.

De este modo ya podemos manipular los componentes VCL sin preocupación.

Modificamos la definición de la clase:

type
TContador = class(TThread)
dwTiempo: DWord;
iSegundos: Integer;
Etiqueta: TLabel;
constructor Create; reintroduce; overload;
procedure Execute; override;
procedure ActualizarPantalla;
end;

Y la implementación de ActualizarPantalla es la siguiente:

procedure TContador.ActualizarPantalla;
begin
Etiqueta.Caption := IntToStr(iSegundos);
end;

Entonces sólo hay que sincronizar el hilo con la VCL en el procedimiento Execute:

procedure TContador.Execute;
begin
inherited;
OnTerminate := Terminar;

// Contamos hasta 10 segundos
while (iSegundos < 10) and not Terminated do
begin
// ¿Han pasado 1000 milisegundos?
if GetTickCount - dwTiempo > 1000 then
begin
// Incrementamos el contador de segundos y
// actualizamos la etiqueta
Inc(iSegundos);
Synchronize(ActualizarPantalla);
dwTiempo := GetTickCount;
end;
end;
end;

Llamamos al procedimiento ActualizarPantalla utilizando Synchronize. Y no sólo podemos utilizar este método para sincronizarnos con los componentes VCL sino que también con variables globales.

Ahora vamos otro caso de dos hilos incrementando paralelamente una misma variable que hace de contador. Tenemos este formulario:

Tenemos dos listas (TListBox) que nos van a mostrar el valor de la variable global i que van a incrementar dos hilos a la vez cuya clase es la siguiente:

type
THilo = class(TThread)
Lista: TListBox;
procedure Execute; override;
procedure MostrarContador;
end;

Y esta sería su implementación:

{ THilo }

procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
i := i + 1;
Synchronize(MostrarContador);
Sleep(1000);
end;
end;

El procedimiento Execute incrementa la variable entera global i, la muestra por pantalla en su lista correspondiente y espera un segundo. Luego tenemos el procedimiento que muestra el valor de la variable i según el hilo:

procedure THilo.MostrarContador;
begin
Lista.Items.Add('i='+IntToStr(i));
end;

Cuando pulsemos el botón Comenzar, ponemos el contador i a cero, creamos los dos hilos, les asignamos su lista correspondiente y los ponemos en marcha:

procedure TFDosHilos.BComenzarClick(Sender: TObject);
begin
i := 0;
Hilo1 := THilo.Create(False);
Hilo2 := THilo.Create(False);
Hilo1.Lista := Lista1;
Hilo2.Lista := Lista2;
Hilo1.Resume;
Hilo2.Resume;
end;

En cualquier momento podemos detener la ejecución de ambos hilos pulsando el botón Detener:

procedure TFDosHilos.BDetenerClick(Sender: TObject);
begin
Hilo1.Terminate;
Hilo2.Terminate;
end;

Al ejecutarlo conforme está ahora mismo este sería el resultado:


Aquí hay algo que no encaja. Si los dos hilos incrementan una vez la variable i sólo deberían verse números pares o todo caso el valor de un hilo debería ser distinto al del otro, pero eso es lo que pasa cuando no van sincronizados los incrementos de la variable i.

Sin tocar el código fuente, vuelvo a ejecutar el programa y me da este otro resultado:


¿Qué está pasando? Pues que el hilo 2 puede incrementar la variable i justo después de que la incremente el hilo 1 o bien pueden hacerlo los dos a la vez, provocando un retraso en el contador.

Esto podemos solucionarlo sincronizando el incremento de la variable i dentro de un nuevo procedimiento que podemos añadir a la clase THilo:

THilo = class(TThread)
Lista: TListBox;
procedure Execute; override;
procedure MostrarContador;
procedure IncrementarContador;
end;

El procedimiento IncrementarContador sólo hace esto:

procedure THilo.IncrementarContador;
begin
i := i + 1;
end;

Luego hacemos que el procedimiento Execute sincronice el incremento de la variable i:

procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(IncrementarContador);
Synchronize(MostrarContador);
Sleep(1000);
end;
end;

Este sería el resultado al ejecutarlo:


Vemos ahora que los incrementos de la variable i son de dos en dos y evitamos que un hilo estropee el incremento de otro. Aunque esto no soluciona a la perfección la sincronización ya que si por ejemplo tenemos otra aplicación (como el navegador Firefox) que ralentiza el funcionamiento de Windows, podría provocar que se relentice uno de los hilos respecto al otro y provoque de nuevo la descoordinación en los incrementos. Eso veremos como solucionarlo en el siguiente artículo mediante semáforos y eventos.

PARAR, REANUDAR, DETENER O ESPERAR LA EJECUCIÓN DEL HILO

Siguiendo con el primer ejemplo (la clase TContador), una vez el hilo está en marcha podemos suspenderlo (manteniendo su estado) utilizando el método Suspend:

Contador.Suspend;

Para que continúe sólo hay que volver a llamar al procedimiento Resume:

Contador.Resume;

En cualquier momento podemos saber si está suspendido el hilo mediante la propiedad:

if contador.Suspended then
...

Si queremos detener definitivamente la ejecución del hilo entonces deberíamos añadirle al bucle principal de nuestro procedimiento Execute esta comprobación:

// Contamos hasta 10 segundos
while (iSegundos < 10) and not Terminated do
begin
...

Entonces podíamos añadir al formulario un botón llamado Detener que al pulsarlo haga esto:

Contador.Terminate;

Lo que hace realmente el procedimiento Terminate no es terminar el hilo de ejecución, sino poner la propiedad Terminated a True para que nosotros salgamos lo antes posible del bucle infinito en el que está sumergido nuestro hilo. Lo que nunca hay que intentar es hacer esto para terminar un hilo:

Contador.Free;

Lo que es terminar, seguro que termina, pero la explosión la tenemos asegurada. Para cerrar el hilo inmediatamente podemos llamar a una función de la API de Windows llamada TerminateThread:

TerminateThread(Contador.Handle, 0);

El primer parámetro es el Handle del hilo que queremos detener y el según parámetro es el código de salida que puede ser leído posteriormente por la función GetExitCodeThread. Recomiendo no utilizar la función TerminateThread a menos que sea estrictamente necesario. Es mejor utilizar el procedimiento Terminate y procurar salir limpiamente del bucle del hilo cerrando los asuntos que tengamos a medio (cerrando ficheros abiertos, liberando memoria de objetos creados, etc.).

Por otro lado tenemos el procedimiento DoTerminate que no termina el hilo de ejecución, sino que provoca el evento OnTerminate en el cual podemos colocar todo lo que necesitamos para terminar nuestro hilo. Por ejemplo, podemos poner al comienzo de nuestro procedimiento Execute:

procedure TContador.Execute;
begin
inherited;
OnTerminate := Terminar;
...

Y luego definimos dentro de nuestra clase TContador el evento Terminar:

procedure TContador.Terminar(Sender: TObject);
begin
// Escribimos aquí el código que necesitamos
// para liberar los recursos del hilo y luego
// le mandamos una señal para que termine el bucle principal
Terminate;
end;

También podemos hacer que el hilo principal de la aplicación espere a que termine la ejecución de un hilo antes de seguir ejecutando código. Esto se consigue con el procedimiento WaitFor:

Contador.WaitFor;

Aunque esto puede ser algo peligroso porque no permite que Windows procese los mensajes de nuestra ventana. Podría utilizarse por ejemplo para ejecutar un programa externo (de MSDOS) y esperar a que termine su ejecución.

En el siguiente artículo veremos como podemos controlar las esperas entre el hilo principal y los hilos secundarios para que se coordinen en sus trabajos.

Pruebas realizadas en RAD Studio 2007.

08 mayo 2009

Los Hilos de Ejecución (1)

Hace tiempo escribí un pequeño post sobre como crear hilos de ejecución:

Cómo crear un hilo de ejecución

Pero debido a que últimamente los procesadores multinúcleo van tomando más relevancia y que algunos lectores de este blog han pedido que me extienda en el tema, voy a meterme de lleno en el apasionante y peligroso mundo de las aplicaciones multihilo.

Cuando manejamos Windows creemos que es un sistema operativo robusto que puede ejecutar gran cantidad de tareas a la vez. Eso es lo que parece externamente, pero realmente sólo tiene dos hilos de ejecución en los últimos procesadores de doble núcleo. Y en procesadores Pentium 4 e inferiores sólo contiene un solo hilo de ejecución (dos mediante el disimulado HyperThreading que tantos problemas nos dio con Interbase 6).

Entonces, ¿Cómo puede ejecutar varias tareas a la vez? (Emule, Bittorrent, MSN, reproductor multimedia, etc.). Primero tenemos que ver lo que significa un proceso.

LOS PROCESOS

Un proceso (un programa EXE o un servicio de Windows) contiene como mínimo un hilo, llamado hilo primario. Si queremos que nuestro proceso tenga más hilos, se lo tenemos que pedir al sistema operativo. Estos nuevos hilos pueden ejecutarse paralelamente al hilo primario y ser independientes.

Un proceso se compone principalmente de estas tres áreas:

AREA DE CÓDIGO: Es una zona de memoria de sólo lectura donde está compilado nuestro programa en código objeto (binario).

HEAP (MONTÓN): Es en esta zona de memoria de lectura/escritura donde se suelen guardar las variables globales que creamos así como las instancias de los objetos que se van creando en memoria.

PILA: Aquí se guardan temporalmente los parámetros que se pasan entre funciones así como las direcciones de llamada y retorno de procedimientos y funciones. Todos los datos que se introducen en la pila tienen que salir mediante el método LIFO (lo último que entra es lo primero que tiene que salir) porque si no provoca un desbordamiento del puntero del procesador que hace que retorne a otra zona de memoria provocando el error Access Violation 000000 o FFFFFF, es decir, intenta salirse del segmento de memoria de nuestra aplicación.

Estas tres zonas se encuentran herméticamente separadas las unas de las otras y si por error un comando de nuestro programa intenta acceder fuera del heap o de la pila provocará el conocido mensaje que tanto nos gusta: Access Violation. Suele ocurrir al intentar acceder a un objeto que no ha sido creado en el heap (dirección 000000 = nil), etc.

El conjunto de estas tres zonas de memoria es el proceso. Ahora bien, si creamos un nuevo hilo de ejecución dentro del proceso, éste tendrá su propia pila, aunque compartirá la misma zona de datos (heap).

EL PROCESO MULTITAREA

Windows realiza la multitarea cediendo un pequeño tiempo de procesador a cada proceso, de modo que en un solo segundo pueden ejecutarse ciertos de procesos simultáneos. Para los viejos roqueros que estudiamos ensamblador vimos que antes de cambiar de tarea Windows guarda el estado de todos los registros en la pila:

PUSH EAX
PUSH EBX
...

Para luego restaurarlos y seguir su marcha:

...
POP EBX
POP EAX

No está de más adquirir unos pequeños conocimientos de ensamblados de x86 para conocer a fondo las tripas de la máquina.

Cuando Windows para el control de un proceso, memoriza el estado de los registros del procesador y la pila y pasa al siguiente proceso hasta que finalice su tiempo. Realmente, este cambio de procesos no lo realiza Windows, sino el mismo procesador que trae esta característica por hardware (desde el 80386. Hasta el procesador 80286 tenía este modo multitarea aunque sólo en 16 bits).

Pero lo que vamos a ver es este artículo es un cambio de ejecución entre hilos del mismo proceso, algo que consume muchos menos recursos que el cambio entre procesos.

Por ejemplo, los navegadores webs modernos actuales como Firefox o Chrome ejecutan cada web dentro del mismo proceso pero en hilos diferentes (incluso Chrome lo hace en distintos procesos), de modo que si una página web se queda colgada no afecta a otra situada en la siguiente pestaña (lo que no se es si llamarán al sistema de hilos de Windows y tendrán su propio núcleo duro de ejecución, su propio sistema multitarea).

Pero programar hilos de ejecución no está exento de problemas. ¿Qué pasaría si dos hilos acceden a la vez a la misma variable de memoria? O peor aún, ¿Y si intentan escribir a la vez en una unidad de CD-ROM? Las excepciones que pueden ocurrir pueden ser catastróficas, aunque afortunadamente a partir de Windows NT y XP se controlan muy bien todos estos problemas de concurrencia.

Lo que si es realmente difícil es intentar depurar dos hilos de ejecución que utilizan los mismos recursos del proceso. De ahí a que sólo hay que recurrir a los hilos en casos estrictamente necesarios.

LA CLASE TTHREAD

Toda la complejidad de los hilos de ejecución que se programan en la API de Windows quedan encapsulados en Delphi en esta sencilla clase. Para crear un hilo de ejecución basta heredar de la clase TThread y sobrecargar el método Execute:

type
THilo = class(TThread)
procedure Execute; override;
end;

En la implementación del procedimiento Execute es donde hay que introducir el código que queremos ejecutar cuando arranque nuestro hilo de ejecución:

{ THilo }

procedure THilo.Execute;
begin
inherited;

end;

Veamos un ejemplo sencillo de un hilo de ejecución con un contador de 10 segundos.

CREANDO UN CONTADOR DE SEGUNDOS

Voy a crear un nuevo proyecto con un formulario en el que sólo va a haber una etiqueta (TLabel) llamada EContador con el contador de segundos:


A este formulario lo he llamado FPrincipal. En la interfaz del mismo voy a definir la clase TContador que va a heredar de un hilo TThread:

type
TContador = class(TThread)
dwTiempo: DWord;
iSegundos: Integer;
Etiqueta: TLabel;
constructor Create; reintroduce; overload;
procedure Execute; override;
end;

He sobrecargado el constructor para que inicialice mis contadores de tiempo y segundos:

constructor TContador.Create;
begin
inherited Create(True); // llamamos al constructor del padre (TThread)
dwTiempo := GetTickCount;
iSegundos := 0;
end;

La función GetTickCount nos devuelve un número entero que representa el número de milisegundos que han pasado desde que encendimos nuestro PC. Si queréis más precisión podéis utilizar la función TimeGetTime declarada en la unidad MMSystem (sobre todo si vais a programar videojuegos).

Siguiendo con la implementación de nuestra clase TContador, la variable dwTiempo la voy a utilizar para controlar el número de milisegundos que van pasando desde que ejecutamos el hilo. Y la variable iSegundos es un contador de segundos de 0 a 10. También he añadido una referencia a una Etiqueta de tipo TLabel que se la daremos al crear una instancia del hilo en el evento OnCreate del formulario principal:

procedure TFPrincipal.FormCreate(Sender: TObject);
begin
Contador := TContador.Create(True);
Contador.Etiqueta := EContador;
Contador.FreeOnTerminate := True;
Contador.Resume;
end;

Después de crear el hilo le pasamos la etiqueta que tiene que actualizar y le decimos mediante la propiedad FreeOnTerminate que se libere de memoria automáticamente al terminar la ejecución del hilo.

Después llamamos al método Resume que lo que hace internamente es ejecutar el procedimiento Execute de la clase TThread que tendrá este código:

procedure TContador.Execute;
begin
inherited;

// Contamos hasta 10 segundos
while iSegundos < 10 do
// ¿Han pasado 1000 milisegundos?
if GetTickCount - dwTiempo > 1000 then
begin
// Incrementamos el contador de segundos y actualizamos la etiqueta
Inc(iSegundos);
Etiqueta.Caption := IntToStr(iSegundos);
dwTiempo := GetTickCount;
end;
end;

Creamos un bucle cerrado que va contando de 1000 en 1000 milisegundos e incrementando el contador de segundos. Cuando se salga de este bucle se terminará la ejecución del hilo automáticamente. También se podía haber utilizado procedimiento Sleep, pero nunca me ha gustado mucho esta función porque miestras se está ejecutando no podemos hacer absolutamente nada.

Eso ocurrirá al ejecutar el programa y cuando llegue el contador a 10:


Podemos ver como va la ejecución del hilo en la ventana inferior de Delphi si estamos ejecutando el programa en modo depuración:


Como puede verse en las líneas en rojo, el hilo que hemos creado tiene el ID 3736 que no tiene nada que ver con el hilo primario:


En el siguiente artículo seguiremos profundizando un poco más en los hilos de ejecución a través de otros ejemplos.

Pruebas realizadas en RAD Studio 2007.

04 septiembre 2007

Explorar unidades y directorios

Si importante es controlar el manejo de archivos no menos importante es el saber moverse por las unidades de disco y los directorios.

Veamos que tenemos Delphi para estos menesteres:

function CreateDir( const Dir: string ): Boolean;

Esta función crea un nuevo directorio en la ruta indicada por Dir. Devuelve True o False dependiendo si ha podido crearlo o no. El único inconveniente que tiene esta función es que deben existir los directorios padres. Por ejemplo:

CreateDir( 'C:\prueba' ) devuelve True
CreateDir( 'C:\prueba\documentos' ) devuelve True
CreateDir( 'C:\otraprueba\documentos' ) devuelve False (y no lo crea)

function ForceDirectories( Dir: string ): Boolean;

Esta función es similar a CreateDir salvo que también crea toda la ruta de directorios padres.

ForceDirectories( 'C:\prueba' ) devuelve True
ForceDirectories( 'C:\prueba\documentos' ) devuelve True
ForceDirectories( 'C:\otraprueba\documentos' ) devuelve True


procedure ChDir( const S: string ); overload;

Este procedimiento cambia el directorio actual al indicado por el parámetro S. Por ejemplo:

ChDir( 'C:\Windows\Fonts' );

function GetCurrentDir: string;

Nos devuelve el nombre del directorio actual donde estamos posicionados. Por ejemplo:

GetCurrentDir devuelve C:\Windows\Fonts

function SetCurrentDir( const Dir: string ): Boolean;

Establece el directorio actual devolviendo True si lo ha conseguido. Por ejemplo:

SetCurrentDir( 'C:\Windows\Java' );

procedure GetDir( D: Byte; var S: string );

Devuelve el directorio actual de una unidad y lo mete en la variable S. El parámetro D es el número de la unidad siendo:

D Unidad
--- ------------------
0 Unidad por defecto
1 A:
2 B:
3 C:
...

Por ejemplo para leer el directorio actual de la unidad C:

var
sDirectorio: String;
begin
GetDir( 3, sDirectorio );
ShowMessage( 'El directorio actual de la unidad C: es ' + sDirectorio );
end;

function RemoveDir( const Dir: string ): Boolean;

Elimina un directorio en el caso de que este vacío, devolviendo False si no ha podido hacerlo.

RemoveDir( 'C:\prueba\documentos' ) devuelve True
RemoveDir( 'C:\prueba' ) devuelve True
RemoveDir( 'C:\otraprueba' ) devuelve False porque no esta vacío

function DirectoryExists( const Directory: string ): Boolean;

Comprueba si existe el directorio indicado por el parámetro Directory. Por ejemplo:

DirectoryExists( 'C:\Windows\System32\' ) devuelve True
DirectoryExists( 'C:\Windows\MisDocumentos\' ) devuelve False

function DiskFree( Drive: Byte ): Int64;

Devuelve el número de bytes libres de una unidad de dico indicada por la letra Drive:

Drive Unidad
----------- ------
0 Unidad por defecto
1 A:
2 B:
3 C:
...

Por ejemplo vamos a ver el número de bytes libres de la unidad C:

DiskFree( 3 ) devuelve 5579714560


function DiskSize( Drive: Byte ): Int64;

Nos dice el tamaño total en bytes de una unidad de disco. Por ejemplo:

DiskSize( 3 ) devuelve 20974428160


BUSCANDO ARCHIVOS DENTRO DE UN DIRECTORIO

Para buscar archivos dentro de un directorio disponemos de las funciones:

function FindFirst( const Path: string; Attr: Integer; var F: TSearchRec ): Integer;

Busca el primer archivo, directorio o unidad que se encuentre dentro de una ruta en concreto. Devuelve un cero si ha encontrado algo. El parámetro TSearchRec es una estructura de datos donde se almacena lo encontrado:

type
TSearchRec = record
Time: Integer;
Size: Integer;
Attr: Integer;
Name: TFileName;
ExcludeAttr: Integer;
FindHandle: THandle;
FindData: TWin32FindData;
end;


function FindNext( var F: TSearchRec ): Integer;

Busca el siguiente archivo, directorio o unidad especificado anteriormente por la función FindFirst. Devuelve un cero si ha encontrado algo.

procedure FindClose( var F: TSearchRec );

Este procedimiento cierra la búsqueda comenzada por FindFirst y FindNext.

Veamos un ejemplo donde se utilizan estas funciones. Vamos a hacer un procedimiento que lista sólo los archivos de un directorio que le pasemos y vuelca su contenido en un StringList:

procedure TFPrincipal.Listar( sDirectorio: string; var Resultado: TStringList );
var
Busqueda: TSearchRec;
iResultado: Integer;
begin
// Nos aseguramos que termine en contrabarra
sDirectorio := IncludeTrailingBackslash( sDirectorio );

iResultado := FindFirst( sDirectorio + '*.*', faAnyFile, Busqueda );

while iResultado = 0 do
begin
// ¿Ha encontrado un archivo y no es un directorio?
if ( Busqueda.Attr and faArchive = faArchive ) and
( Busqueda.Attr and faDirectory <> faDirectory ) then
Resultado.Add( Busqueda.Name );

iResultado := FindNext( Busqueda );
end;

FindClose( Busqueda );
end;

Si listamos el raiz de la unidad C:

var
Directorio: TStringList;
begin
Directorio := TStringList.Create;
Listar( 'C:', Directorio );
ShowMessage( Directorio.Text );
Directorio.Free;
end;

El resultado sería:

AUTOEXEC.BAT
Bootfont.bin
CONFIG.SYS
INSTALL.LOG
IO.SYS
MSDOS.SYS
NTDETECT.COM

Con estas tres funciones se pueden hacer cosas tan importantes como eliminar directorios, realizar búsquedas de archivos, calcular lo que ocupa un directorio en bytes, etc.

Pruebas realizadas en Delphi 7.

03 septiembre 2007

Trabajando con archivos de texto y binarios (y V)

Vamos a terminar de ver los recursos de los que dispone Delphi para el tratamiento de archivos.

PARTIENDO UN ARCHIVO EN DOS

En este ejemplo vamos a coger el archivo prueba.dat de 100 Kb y dejamos sólo las primeras 30 Kb eliminando el resto:

procedure TFormulario.PartirArchivo;
var F: File of byte;
Buffer: array[0..1023] of Byte;
i: Integer;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'prueba.dat' );
Reset( F );

// Leemos 30 Kb utilizando un buffer de 1 Kb
for i := 1 to 30 do
BlockRead( F, Buffer, 1024 );

Truncate( F );
CloseFile( F );
end;

Hemos utilizado para ello la función Truncate, la cual parte el archivo que estamos leyendo según donde este el puntero F.


ASEGURANDO QUE SE GUARDE LA INFORMACIÓN EN ARCHIVOS DE TEXTO

Cuando abrimos un archivo de texto para escribir en él no se guarda completamente toda la información hasta que se cierra con el procedimiento CloseFile. Esto puede dar problemas si en algún momento el procedimiento WriteLn provoca un error dejando el archivo abierto. Se perdería la mayor parte de la información que supuestamente debería haberse guardado en el mismo.

Para evitar esto disponemos de la función Flush:

function Flush( var t: TextFile ): Integer;

Esta función lo que hace es vaciar el buffer del archivo de texto en el disco duro. Es algo así como ejecutar CloseFile pero sin cerrar el archivo. Por ejemplo:

var
F: TextFile;
sArchivo: String;
begin
sArchivo := ExtractFilePath( Application.ExeName ) + 'prueba.txt';
AssignFile( F, sArchivo );
Rewrite( F );
WriteLn( F, 'Esto es el contenido del archivo de texto.' );
Flush( F );
// aquí podemos seguir escribiendo en el mismo
CloseFile( F );
end;

GUARDANDO OTROS TIPOS DE DATOS EN LOS ARCHIVOS

Hasta ahora sólo hemos guardado información de tipo texto y binaria en archivos pero se puede guardar cualquier tipo de información utilizando cualquier tipo de dato. Por ejemplo para guardar una serie de números reales se haría de la siguiente manera:

procedure TFormulario.GuardarRecibos;
var
F: File of Real;
r: Real;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'ImporteRecibos.dat' );
Rewrite( F );
r := 120.45;
Write( F, r );
r := 1800.05;
Write( F, r );
r := 66.31;
Write( F, r );
CloseFile( F );
end;

Si os fijais bien en el archivo resultante vemos que ocupa 24 bytes. Esto es así porque el tipo real ocupa 8 bytes en memoria y como son 3 números reales lo que hemos guardado entonces hace un total de 24 bytes. Hay que asegurarse de que tanto al leer como al guardar información en este tipo de archivos se haga con el mismo tipo de variable. Si guardamos información con File of Real y la leemos con File of Single los resultados podrían ser catastróficos (a parte de quedarse el archivo a medio leer).

También se pueden guardar estructuras de datos almacenadas en registros. En este ejemplo que voy a mostrar vamos a ver como guardar los datos de un cliente en un archivo:

type
TCliente = record
ID: Integer;
sNombre: String[50];
rSaldo: Real;
bPagado: Boolean;
end;

procedure TFPrincipal.CrearArchivoRegistro;
var
Cliente: TCliente;
F: File of TCliente;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'clientes.dat' );
Rewrite( F );

with Cliente do
begin
ID := 1;
sNombre := 'FRANCISCO MARTINEZ LÓPEZ';
rSaldo := 1200.54;
bPagado := False;
end;

Write( F, Cliente );

with Cliente do
begin
ID := 2;
sNombre := 'MARIA ROJO PALAZÓN';
rSaldo := 622.32;
bPagado := True;
end;

Write( F, Cliente );

CloseFile( F );
end;

Unas de las cosas a tener en cuenta es que cuando se define una estructura de datos no se puede definir una variable de tipo String sin dar su tamaño, ya que cada registro debe tener una longitud fija. Si ponemos String nos da un error al compilar, por eso hemos puesto en el nombre un String[50].

Con esto finalizamos la parte básica de tratamiento de archivos en Delphi.

Pruebas realizadas en Delphi 7.

Publicidad