Embarcadero Delphi
Ejecutar un hilo manteniendo GUI sensible
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.