O reservar porciones de código sin usar Threads
El título que usé es porque así lo busqué en el momento que lo necesité, pero solo encontré código para esperar cuando se lanza un ejecutable externo o cuando se usan directamente threads.
El problema surge cuando los threads no son manejados directamente por nosotros. En mi caso estoy trabajando con Intraweb (VCL for the Web) y cada vez que se abre un navegador se está ejecutando un hilo de ejecución distinta.
Hay ciertas partes del código a los que acceden todos los hilos y en ciertos casos, si se ejecutan a la vez pueden dar errores. Esto ocurre normalmente con procesos que son “de clase”, ya que no instancian una clase (entonces cada hilo instanciaría una distinta). Los ejemplos de errores pueden ser cuando varios hilos acceden a un archivo o cuando tratan de acceder a una tabla de memoria y mueven su índice.
¿Cómo solucionar este problema? A mi se me ocurrió (seguro que hay formas mucho mejores) que “de alguna forma” cuando un proceso “delicado” va a ejecutarse, pregunte si no esta reservado y usándose y que espere hasta que el otro proceso lo libere. Esto, más o menos, es la definición de pila de ejecución cuando uno usa Threads, pero como dije antes, en ciertas partes del código no tengo acceso al control de ellos.
Así que hice una clase que reserva un proceso o porción de código y hasta que no se libera no continúa. Aprovechando que estoy usando Delphi XE, voy a usar una colección genérica (un excelente pdf con las colecciones genéricas) del tipo TDictionary (aunque se podría haber hecho con un TStringList) y voy a usar también la clase que expliqué ayer en el post Medir tiempos en Delphi.
type
TDicRes = TDictionary<string, Boolean>;
TReservas = classprivateclass var Dic: TDicRes;
class var TiempoEsperado: Int64;
class function ValorReserva(Proc: string):Boolean;class function DicReservas:TDicRes;
class function Reservar(Proc: string):Boolean;publicclass procedure DesReservar(Proc: string);class procedure EsperarUso(Proc: string);class function EsperarYReservar(Proc: string; TiempoMs: Int64 = 0):Boolean;class procedure DestruirReservas;
class function TiempoDeEsperaInt:Int64; overload;class function TiempoDeEspera:String; overload;end;
¿Por qué defino el TDicRec como la clase TDictionary? Porque tenía que devolverlo en un método. Si se fijan, todos los métodos son de clase, esto es porque así no hay que mantener ningún objeto y el Reservar o Desreservar se pueden llamar desde cualquier lado.
Obviamente, al usar un objeto que puede ser accedido en cualquier método, tengo que asegurar que exista. Una forma podría haber sido crearlo en otro método público y que sea llamado al inicio del programa, pero eso genera que haya algo en memoria que puede no utilizarse. Por lo que dentro del código existe el método DicReservas:
class function TReservas.DicReservas: TDicRes;
begin
//Chequea la variable de clase, si es nil la crea
if Dic = nil thenDic := TDicRes.Create;//Siempre devuelve la variable de clase
Result := Dic;end;
Y creo otro método, tambien privado, pero de clase, que chequea si existe el valor en el diccionario y si existe, devuelve el valor del mismo:
class function TReservas.ValorReserva(Proc: string): Boolean;begin
//Por defecto nunca está reservado
Result := False;//Chequea si contiene la clave
if DicReservas.ContainsKey(Proc) then//Si la tiene devuelve el valor
DicReservas.TryGetValue(Proc, Result);end;
Con estos dos métodos en mente, vamos a los principales, el que reserva y el que desreserva. El que reserva es privado porque la idea es que solo pueda reservar un proceso si espera hasta que termine otro, que es el método EsperarYReservar.
class function TReservas.Reservar(Proc: string): Boolean;begin
Result := False;//Si no esta reservado ya, lo reserva
if not ValorReserva(Proc) thenbegin
Result := True;//Agrega al diccionario o cambia el valor
DicReservas.AddOrSetValue(Proc, Result);end;
end;
class procedure TReservas.DesReservar(Proc: string);begin
//Si no existía en el diccionario lo agrega, pero siempre con False
DicReservas.AddOrSetValue(Proc, False);end;
class function TReservas.EsperarYReservar(Proc: string; TiempoMs: Int64 = 0): Boolean;var
//Para más información de esta clase ver el post:
//http://arsenio-programa.blogspot.com/2011/11/medir-tiempos-en-delphi.html
Counter : TPerformanceTime;begin
//Variable global que tiene el último tiempo esperado
TiempoEsperado := 0;Result := False;//Si no envían un tiempo pongo el máximo (un día en milisegundos)
if TiempoMs = 0 thenTiempoMs := TIEMPO_ESPERA_MAX;//Usa la clase que mide tiempos
Counter := TPerformanceTime.Create;try//Inicio el contador
Counter.Start;//Mientras este reservado y el tiempo máximo no sea mayor al que ya pasó
while ValorReserva(Proc) and (Counter.TiempoActual <= TiempoMs) dobegin
//Tiempo que espera, por defecto 100 milisegundos
Sleep(TIEMPO_ESPERA_RES);end;
//Termina el contador
Counter.Finish;//Guarda el tiempo que tardó, para que puedan accederlo desde afuera
TiempoEsperado := Counter.TiempoMS;//Llama a reservar, si aún está reservado esto devuelve false.
Result := Reservar(Proc);finallyCounter.Destroy;end;
end;
Para explicar como utilizarlo voy a mostrar el ejemplo del acceso a un archivo log, al cual tienen acceso desde todo el sistema, ya que es el que uso para guardar la información de ciertos pasos y poder encontrar errores. Este ejemplo está en la misma unidad que les dejo abajo para descargar.
class procedure TLogueoEnArchivo.Log(const Texto: string);var
myFile : TextFile;begin
if Loguear thenbegin
//Acá es donde reserva el proceso de "acceso al archivo"
if TReservas.EsperarYReservar(ArchivoLog, 0) thenbegin
tryAssignFile(myFile, ArchivoLog);//Si el archivo ya existe lo abre con Append
if FileExists(ArchivoLog) thenSystem.Append(myFile)else
begin
//Si no existe lo abre y agrega la primera línea
System.Rewrite(myFile);Writeln(myFile, TimeToStr(Now) + ' - ' + 'INICIO DEL LOG');end;
//Escribe en el archivo incluyendo la hora
Writeln(myFile, TimeToStr(Now) + ' - ' + Texto);finallyCloseFile(myFile);//Importante que el Desreservar esté en un finally, para que si hay un error en el acceso
//al archivo no quedan colgados los demás procesos.
TReservas.DesReservar(ArchivoLog);end;
end;
//Acá podría haber un else por si pasó el tiempo de reserva
//y usar el método TReservas.TiempoDeEspera
end;
end;
Lo único que queda pendiente es destruir el diccionario de reservas usado, para eso en el Destroy de nuestro formulario principal hay que agregar un:
//Destruye las reservas multi-threading
TReservas.DestruirReservas;
Que ejecuta el método:
class procedure TReservas.DestruirReservas;
begin
//Destruye el diccionario si fue creado
if Dic <> nil thenDic.Destroy;end;
Como en los demás post, les dejo el enlace para descargarse el código fuente, que está más completo que lo que solo coloco acá. Si alguno le quedó alguna duda tiene los comentarios, o me puede escribir a mi correo.
Código fuente:
No hay comentarios:
Publicar un comentario