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.

Publicidad