1 de diciembre de 2011

Esperar que un proceso termine en Delphi

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 = class
  private
    class var Dic: TDicRes;
    class var TiempoEsperado: Int64;
    class function ValorReserva(Proc: string):Boolean;
    class function DicReservas:TDicRes;
    class function Reservar(Proc: string):Boolean;
  public
    class 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 then
    Dic := 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) then
  begin
    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 then
    TiempoMs := 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) do
    begin
      //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);
  finally
    Counter.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 then
  begin
    //Acá es donde reserva el proceso de "acceso al archivo"
    if TReservas.EsperarYReservar(ArchivoLog, 0) then
    begin
      try
        AssignFile(myFile, ArchivoLog);
        //Si el archivo ya existe lo abre con Append
        if FileExists(ArchivoLog) then
          System.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);
      finally
        CloseFile(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 then
    Dic.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