Buscar..


Interfaz gráfica de usuario receptiva que usa hilos para trabajos en segundo plano y PostMessage para informar sobre los hilos

Mantener una interfaz gráfica de usuario receptiva mientras se ejecuta un proceso largo requiere o bien algunas "devoluciones de llamada" muy elaboradas para permitir que la interfaz gráfica de usuario procese su cola de mensajes, o el uso de subprocesos (de fondo) (trabajador).

Iniciar cualquier cantidad de subprocesos para hacer algún trabajo por lo general no es un problema. La diversión comienza cuando desea que la GUI muestre resultados intermedios y finales o informe sobre el progreso.

Mostrar cualquier cosa en la GUI requiere interactuar con los controles y / o la cola / bomba de mensajes. Eso siempre debe hacerse en el contexto del hilo principal. Nunca en el contexto de cualquier otro hilo.

Hay muchas maneras de manejar esto.

Este ejemplo muestra cómo puede hacerlo utilizando subprocesos simples, permitiendo que la GUI acceda a la instancia del subproceso después de que termine configurando FreeOnTerminate en false e informando cuando un subproceso se "hace" con PostMessage .

Notas sobre las condiciones de carrera: las referencias a los subprocesos de trabajo se mantienen en una matriz en el formulario. Cuando se termina un hilo, la referencia correspondiente en la matriz se anula.

Esta es una fuente potencial de condiciones de carrera. Como es el uso de un booleano "En ejecución" para que sea más fácil determinar si todavía hay algún hilo que necesita terminar.

Tendrá que decidir si necesita proteger estos recursos utilizando bloqueos o no.

En este ejemplo, tal como está, no hay necesidad. Solo se modifican en dos ubicaciones: el método StartThreads y el método HandleThreadResults . Ambos métodos solo se ejecutan en el contexto del hilo principal. Mientras lo mantengas así y no comiences a llamar estos métodos desde el contexto de diferentes hilos, no hay forma de que produzcan condiciones de carrera.

Hilo

type
  TWorker = class(TThread)
  private
    FFactor: Double;
    FResult: Double;
    FReportTo: THandle;
  protected
    procedure Execute; override;
  public
    constructor Create(const aFactor: Double; const aReportTo: THandle);

    property Factor: Double read FFactor;
    property Result: Double read FResult;
  end;

El constructor simplemente establece los miembros privados y establece FreeOnTerminate en False. Esto es esencial ya que permitirá al hilo principal consultar la instancia del hilo para ver su resultado.

El método de ejecución realiza su cálculo y luego publica un mensaje en el identificador que recibió en su constructor para decir que se realizó:

procedure TWorker.Execute;
const
  Max = 100000000;var
  i : Integer;
begin
  inherited;

  FResult := FFactor;
  for i := 1 to Max do
    FResult := Sqrt(FResult);

  PostMessage(FReportTo, UM_WORKERDONE, Self.Handle, 0);
end;

El uso de PostMessage es esencial en este ejemplo. PostMessage "solo" pone un mensaje en la cola de la bomba de mensajes del hilo principal y no espera a que se maneje. Es de naturaleza asíncrona. Si fueras a usar SendMessage , estarías codificándote en un encurtido. SendMessage pone el mensaje en la cola y espera hasta que se haya procesado. En resumen, es síncrono.

Las declaraciones para el mensaje UM_WORKERDONE personalizado se declaran como:

const
  UM_WORKERDONE = WM_APP + 1;
type
  TUMWorkerDone = packed record
    Msg: Cardinal;
    ThreadHandle: Integer;
    unused: Integer;
    Result: LRESULT;
  end;

La const. UM_WORKERDONE usa WM_APP como punto de partida para su valor para garantizar que no interfiera con ningún valor usado por Windows o Delphi VCL (según lo recomendado por MicroSoft).

Formar

Cualquier forma se puede utilizar para iniciar hilos. Todo lo que necesitas hacer es agregar los siguientes miembros:

private
  FRunning: Boolean;
  FThreads: array of record
    Instance: TThread;
    Handle: THandle;
  end;
  procedure StartThreads(const aNumber: Integer);
  procedure HandleThreadResult(var Message: TUMWorkerDone); message UM_WORKERDONE;

Ah, y el código de ejemplo supone la existencia de un Memo1: TMemo; en las declaraciones del formulario, que utiliza para "registro e informes".

Se puede utilizar FRunning para evitar que se FRunning clic en la GUI mientras se está realizando el trabajo. FThreads se utiliza para mantener el puntero de instancia y el identificador de los subprocesos creados.

El procedimiento para iniciar los hilos tiene una implementación bastante sencilla. Comienza con una verificación de si ya hay un conjunto de subprocesos en espera. Si es así, simplemente sale. Si no, establece el indicador en verdadero e inicia los subprocesos proporcionando a cada uno su propio identificador para que sepan dónde publicar su mensaje "terminado".

procedure TForm1.StartThreads(const aNumber: Integer);
var
  i: Integer;
begin
  if FRunning then
    Exit;
    
  FRunning := True;

  Memo1.Lines.Add(Format('Starting %d worker threads', [aNumber]));
  SetLength(FThreads, aNumber);
  for i := 0 to aNumber - 1 do
  begin
    FThreads[i].Instance := TWorker.Create(pi * (i+1), Self.Handle);
    FThreads[i].Handle := FThreads[i].Instance.Handle;
  end;
end;

El identificador del subproceso también se coloca en la matriz porque eso es lo que recibimos en los mensajes que nos dicen que se ha realizado un subproceso y tenerlo fuera de la instancia del subproceso hace que sea un poco más fácil acceder. Tener el identificador disponible fuera de la instancia del hilo también nos permite usar FreeOnTerminate en True si no necesitamos la instancia para obtener sus resultados (por ejemplo, si se han almacenado en una base de datos). En ese caso, por supuesto, no habría necesidad de mantener una referencia a la instancia.

La diversión está en la implementación de HandleThreadResult:

procedure TForm1.HandleThreadResult(var Message: TUMWorkerDone);
var
  i: Integer;
  ThreadIdx: Integer;
  Thread: TWorker;
  Done: Boolean;
begin
  // Find thread in array
  ThreadIdx := -1;
  for i := Low(FThreads) to High(FThreads) do
    if FThreads[i].Handle = Cardinal(Message.ThreadHandle) then
    begin
      ThreadIdx := i;
      Break;
    end;

  // Report results and free the thread, nilling its pointer and handle 
  // so we can detect when all threads are done.
  if ThreadIdx > -1 then
  begin
    Thread := TWorker(FThreads[i].Instance);
    Memo1.Lines.Add(Format('Thread %d returned %f', [ThreadIdx, Thread.Result]));
    FreeAndNil(FThreads[i].Instance);
    FThreads[i].Handle := nil;
  end;

  // See whether all threads have finished.
  Done := True;
  for i := Low(FThreads) to High(FThreads) do
    if Assigned(FThreads[i].Instance) then
    begin
      Done := False;
      Break;
    end;
  if Done then
  begin
    Memo1.Lines.Add('Work done');
    FRunning := False;
  end;
end;

Este método primero busca el hilo usando el identificador recibido en el mensaje. Si se encontró una coincidencia, recupera e informa el resultado del hilo usando la instancia ( FreeOnTerminate era False , ¿recuerdas?), Y luego finaliza: libera la instancia y configura la referencia de la instancia y el identificador a cero, lo que indica que este hilo no es ya relevante

Finalmente, comprueba si alguno de los subprocesos todavía se está ejecutando. Si no se encuentra ninguno, se informa "todo hecho" y el indicador de FRunning establece en False para que se pueda iniciar un nuevo lote de trabajo.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow