Usando TEvent y MsgWaitForMultipleObjects para bloquear el hilo principal

Oct 08 2020

He encontrado este interesante código de Remy. Delphi: ¿Cómo crear y usar Thread localmente?

¿Se puede hacer esto para poder hacer varios subprocesos y esperar hasta que estén todos terminados y luego continuar con el subproceso principal? Lo intenté así pero sin éxito ...

procedure Requery(DataList: TStringList);
var
  Event: TEvent;
  H: THandle;
  OpResult: array of Boolean;
  i: Integer;
begin
  Event := TEvent.Create;
  try
    SetLength(OpResult, DataList.Count); 
    for i:=0 to DataList.Count-1 do begin
      TThread.CreateAnonymousThread(
        procedure
        begin
          try
            // run query in thread
            OpResult[i]:=IsMyValueOK(DataList.Strings[i]);
          finally
            Event.SetEvent;
          end;
        end
      ).Start;
      H := Event.Handle;
    end;
    while MsgWaitForMultipleObjects(1, H, False, INFINITE, QS_ALLINPUT) = (WAIT_OBJECT_0+1) do Application.ProcessMessages;
    
    for i:=Low(OpResult) to High(OpResult) do begin
      Memo1.Lines.Add('Value is: ' + BoolToStr(OpResult[i], True));
    end;
  finally
    Event.Free;
  end;

  // Do next jobs with query
  ...
end;

Respuestas

5 RemyLebeau Oct 08 2020 at 05:31

¿Se puede hacer esto para poder hacer varios subprocesos y esperar hasta que estén todos terminados?

Si. Simplemente necesita crear varios TEventobjetos, uno para cada uno TThread, y luego almacenar todos sus Handles en una matriz para pasar a MsgWaitForMultipleObjects():

procedure Requery(DataList: TStringList);
var
  Events: array of TEvent;
  H: array of THandle;
  OpResult: array of Boolean;
  i: Integer;
  Ret, Count: DWORD;

  // moved into a helper function so that the anonymous procedure
  // can capture the correct Index...
  procedure StartThread(Index: integer);
  begin
    Events[Index] := TEvent.Create;
    TThread.CreateAnonymousThread(
      procedure
      begin
        try
          // run query in thread
          OpResult[Index] := IsMyValueOK(DataList.Strings[Index]);
        finally
          Events[Index].SetEvent;
        end;
      end
    ).Start;
    H[Index] := Events[Index].Handle;
  end;

begin
  if DataList.Count > 0 then
  begin
    SetLength(Events, DataList.Count);
    SetLength(H, DataList.Count);
    SetLength(OpResult, DataList.Count);

    try
      for i := 0 to DataList.Count-1 do begin
        StartThread(i);
      end;

      Count := Length(H);
      repeat
        Ret := MsgWaitForMultipleObjects(Count, H[0], False, INFINITE, QS_ALLINPUT);
        if Ret = WAIT_FAILED then RaiseLastOSError;
        if Ret = (WAIT_OBJECT_0+Count) then
        begin
          Application.ProcessMessages;
          Continue;
        end;
        for i := Integer(Ret-WAIT_OBJECT_0)+1 to High(H) do begin
          H[i-1] := H[i];
        end;
        Dec(Count);
      until Count = 0;

      for i := Low(OpResult) to High(OpResult) do begin
        Memo1.Lines.Add('Value is: ' + BoolToStr(OpResult[i], True));
      end;
    finally
      for i := Low(Events) to High(Events) do begin
        Events[i].Free;
      end;
    end;
  end;

  // Do next jobs with query
  ...
end;

Dicho esto, alternativamente puede deshacerse de los TEventobjetos y esperar en TThread.Handlesu lugar la s. Un subproceso Handlese indica para una operación de espera cuando el subproceso se termina por completo. El único problema es que TThread.CreateAnonymousThread()crea una propiedad de TThreadquién FreeOnTerminatees True, por lo que tendrá que desactivarla manualmente:

procedure Requery(DataList: TStringList);
var
  Threads: array of TThread;
  H: array of THandle;
  OpResult: array of Boolean;
  i: Integer;
  Ret, Count: DWORD;

  // moved into a helper function so that the anonymous procedure
  // can capture the correct Index...
  procedure StartThread(Index: integer);
  begin
    Threads[Index] := TThread.CreateAnonymousThread(
      procedure
      begin
        // run query in thread
        OpResult[Index] := IsMyValueOK(DataList.Strings[Index]);
      end
    );
    Threads[Index].FreeOnTerminate := False;
    H[Index] := Threads[Index].Handle;
    Threads[Index].Start;
  end;

begin
  try
    SetLength(Threads, DataList.Count);
    SetLength(H, DataList.Count);
    SetLength(OpResult, DataList.Count);

    for i := 0 to DataList.Count-1 do begin
      StartThread(i);
    end;

    Count := Length(H);
    repeat
      Ret := MsgWaitForMultipleObjects(Count, H[0], False, INFINITE, QS_ALLINPUT);
      if Ret = WAIT_FAILED then RaiseLastOSError;
      if Ret = (WAIT_OBJECT_0+Count) then
      begin
        Application.ProcessMessages;
        Continue;
      end;
      for i := Integer(Ret-WAIT_OBJECT_0)+1 to High(H) do begin
        H[i-1] := H[i];
      end;
      Dec(Count);
    until Count = 0;

    for i := Low(OpResult) to High(OpResult) do begin
      Memo1.Lines.Add('Value is: ' + BoolToStr(OpResult[i], True));
    end;
  finally
    for i := Low(Threads) to High(Threads) do begin
      Threads[i].Free;
    end;
  end;

  // Do next jobs with query
  ...
end;

De cualquier manera, tenga en cuenta que MsgWaitForMultipleObjects()está limitado a esperar un máximo de 63 ( MAXIMUM_WAIT_OBJECTS[64] - 1) identificadores a la vez. La WaitForMultipleObjects()documentación explica cómo evitar esa limitación, si necesita:

Para esperar en más de MAXIMUM_WAIT_OBJECTS identificadores, utilice uno de los siguientes métodos:

  • Cree un hilo para esperar en los identificadores MAXIMUM_WAIT_OBJECTS, luego espere en ese hilo más los otros identificadores. Utilice esta técnica para dividir los identificadores en grupos de MAXIMUM_WAIT_OBJECTS.
  • Llame a RegisterWaitForSingleObject para esperar en cada identificador. Un subproceso de espera del grupo de subprocesos espera en MAXIMUM_WAIT_OBJECTS objetos registrados y asigna un subproceso de trabajo después de que se señala el objeto o expira el intervalo de tiempo de espera.

O simplemente puede procesar su lista en lotes más pequeños, digamos que no más de 50-60 artículos a la vez.