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:

30 de noviembre de 2011

Medir tiempos en Delphi

Varias veces he necesitado medir tiempos dentro del código de Delphi, sobre todo cuando queremos mejorar la performance de un programa, o saber si determinada funcionalidad incrementó el tiempo, o medir los tiempos de las consultas SQL.

No hay un “medidor” de tiempos, al menos que yo conozca, así que me puse a hacer uno y quizás les pueda servir a ustedes. Acá les dejo lo básico del código y luego un archivo con el código completo.

  TPerformanceTime = Class(TObject)
  private
    FElapsed: Int64;
    FStart, FFinish, FFrec:Int64;
  public
    Funcionando: Boolean;
    procedure Start;
    procedure Finish;
    function TiempoMS: Int64;
    function TiempoActual: Int64;    
    class function TiempoAStringFormateado(aTiempo: Int64): string;
    class function TiempoADateTime(Pasado: int64): TDateTime;
    class function TiempoAString(Formato: String; Tiempo: TDateTime): string;
  end;

La implementación de los dos métodos principales (Start y Finish) y el método que simplemente devuelve el tiempo pasado:

procedure TPerformanceTime.Start;
begin
  FElapsed := 0;
  QueryPerformanceFrequency(FFrec);
  QueryPerformanceCounter(FStart);
  Funcionando := True;
end;

procedure TPerformanceTime.Finish;
begin
  if (Funcionando) then
  begin
    QueryPerformanceCounter(FFinish);
    FElapsed := (FFinish - FStart) * MiliSegundos div FFrec;
    Funcionando := False;
  end;
end;

function TPerformanceTime.TiempoMS: Int64;
begin
  Result := FElapsed;
end;

Usarlo es muy facil, en el procedimiento donde queremos medir tiempos (por ejemplo, antes de hacer un Open de una TQuery), creamos el objeto:

  Counter := TPerformanceTime.Create;

Y en lo posible dentro de un try finally hacemos los siguientes pasos:

  try
    //Inicio el contador
    Counter.Start;
    //codigo que quiero medir su tiempo
    //por ejemplo un Query.Open;
    Counter.Finish;
    //Acá es donde vemos que hacemos con el tiempo obtenido Counter.TiempoMS
    //por ejemplo mostrarlo de forma "leible"
    ShowMessage('Duración -> ' + TPerformanceTime.TiempoAStringFormateado(Counter.TiempoMS));
  finally
    //Destrucción del contador
    Counter.Destroy;
  end;

Y básicamente eso es todo. Obviamente despues se puede ampliar su funcionamiento, por ejemplo el método de clase que llamé “TiempoAStringFormateado” devuelve el tiempo transcurrido usando un formato preestablecido, en mi caso como constantes en la cabecera de la unit:

CONST
  MiliSegundos = 1000;
  FORMATO_TIEMPO_LOG_DIAS = 'dd "Días "hh" Horas "nn" Minutos "ss" Segundos "zzz" Milisegundos"';
  FORMATO_TIEMPO_LOG_HORAS = 'hh" Horas "nn" Minutos "ss" Segundos "zzz" Milisegundos"';
  FORMATO_TIEMPO_LOG_MINUTOS = 'nn" Minutos "ss" Segundos "zzz" Milisegundos"';
  FORMATO_TIEMPO_LOG_SEGUNDOS = 'ss" Segundos "zzz" Milisegundos"';
  FORMATO_TIEMPO_LOG_MILISEGUNDOS = 'zzz" Milisegundos"';

Y el método en si:

class function TPerformanceTime.TiempoAStringFormateado(aTiempo: Int64): string;
var
  Hour, Min, Sec, MSec: Word;
  Tiempo: TDateTime;
begin
  //Pasa los milisegundos a TDateTime
  Tiempo := TiempoADateTime(aTiempo);
  //Extrae del tiempo las horas, minutos, segundos y milisegundos
  DecodeTime(Tiempo, Hour, Min, Sec, MSec);
  //Si al truncarlo hay algún valor es que al menos son días
  if Trunc(Tiempo) > 0 then
  begin
    Result := TiempoAString(FORMATO_TIEMPO_LOG_DIAS, Tiempo);
  end
  //Sino formatea con el valor más alto que encuentra.
  else if (Hour > 0) then
  begin
    Result := TiempoAString(FORMATO_TIEMPO_LOG_HORAS, Tiempo);
  end
  else if (Min > 0) then
  begin
    Result := TiempoAString(FORMATO_TIEMPO_LOG_MINUTOS, Tiempo);
  end
  else if (Sec > 0) then
  begin
    Result := TiempoAString(FORMATO_TIEMPO_LOG_SEGUNDOS, Tiempo);
  end
  else if (MSec > 0) then
  begin
    Result := TiempoAString(FORMATO_TIEMPO_LOG_MILISEGUNDOS, Tiempo);
  end;
end;

El archivo fuente pueden descargarlo desde acá (corregido):
UPerformanceTime.pas