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.

4 comentarios:

Marcos dijo...

Hola. Muy bueno los hilos, pero mi pregunta ¿Sabes como implementar un servicio TService? añadir interface con usuario a través de algun TTrayIcon, etc...

Administrador dijo...

Pues la verdad es que no tengo ni idea de como hacerlo, pero he encontrado un artículo muy interesante en inglés de como crear un servicio:

http://www.tolderlund.eu/delphi/service/service.htm

Algún día de estos lo probaré y escribiré un artículo. La verdad es que no le sacamos partido a todo lo que se puede hacer con Delphi.

Saludos.

Marcos dijo...

Gracias, esta wapo, investigaré y aportaré algo.... un saludo.

Marcos dijo...

Aquí también he encotrado un link para aprender más sobre Hilos. Está en inglés pero vale la pena...
http://www.eonclash.com/Tutorials/Multithreading/MartinHarvey1.1/ToC.html

Publicidad