Почему WaitForMultipleObjects терпит неудачу при использовании нескольких ручек потоков?

В следующей тестовой программе каждый тестовый поток добавляет свой дескриптор в глобальный TThreadList при запуске и удаляет свой дескриптор из того же списка, когда его выполнение вот-вот закончится.

В целях тестирования, кроме того, каждая нить обеспечивает добавление ее дескриптора до того, как основной поток заблокирует список (чтобы дублировать их дескрипторы и начать ждать их завершения). В потоках также убедитесь, что они не удаляют свои ручки до того, как основной поток заблокирует список.

Программа тестирования работает нормально до 50-60 потоков. После этого вызов WaitForMultipleObjects начинает сбой с WAIT_FAILED, GetLastError возвращает 87 (ERROR_INVALID_PARAMETER). В настоящее время он запускает 100 потоков. Мой вопрос: что я делаю неправильно?

Программа скомпилирована с XE2 - обновленной 4, 32-битной целевой платформой. Тест-поле W7x64.

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  windows,
  sysutils,
  classes,
  syncobjs;

type
  TTestThread = class(TThread)
  private
    FAckStarted: TEvent;
    function GetAckHandle: THandle;
    class var
      ThreadList: TThreadList;
      WaitEnd: THandle;
  protected
    procedure Execute; override;
  public
    constructor Create;
    destructor Destroy; override;
    property AckHandle: THandle read GetAckHandle;
  end;

{.$DEFINE FREEONTERMINATE}

constructor TTestThread.Create;
begin
  inherited Create(True);
  FAckStarted := TEvent.Create;
{$IFDEF FREEONTERMINATE}
  FreeOnTerminate := True;
{$ENDIF}
end;

destructor TTestThread.Destroy;
begin
  FAckStarted.Free;
  inherited;
end;

procedure TTestThread.Execute;
begin
//  OutputDebugString(PChar(Format('%d starting -------------', [Handle])));
  ThreadList.Add(Pointer(Handle));
  FAckStarted.SetEvent;

  NameThreadForDebugging(AnsiString(IntToStr(Handle)));

  WaitForSingleObject(WaitEnd, INFINITE);
  ThreadList.Remove(Pointer(Handle));
//  OutputDebugString(PChar(Format('%d leaving -------------', [Handle])));
end;

function TTestThread.GetAckHandle: THandle;
begin
  Result := FAckStarted.Handle;
end;

const
  NumThreads = 100;

var
  DeferThreadEnd: TEvent;
  ThreadList: array of TThread;
  i: Integer;
  Thread: TTestThread;
  WaitThreadStart: THandle;
  LockList: TList;
  LockListCount: Integer;
  ThreadHandleArr: array of THandle;
  WaitRet: DWORD;
begin
  IsMultiThread := True;
  ReportMemoryLeaksOnShutdown := True;

  TTestThread.ThreadList := TThreadList.Create;
  DeferThreadEnd := TEvent.Create;
  TTestThread.WaitEnd := DeferThreadEnd.Handle;

  SetLength(ThreadList, NumThreads);
  for i := 0 to NumThreads - 1 do begin
    Thread := TTestThread.Create;
    ThreadList[i] := Thread;
    WaitThreadStart := Thread.GetAckHandle;
    Thread.Start;
    WaitForSingleObject(WaitThreadStart, INFINITE);
  end;

  LockList := TTestThread.ThreadList.LockList;
  LockListCount := LockList.Count;
  SetLength(ThreadHandleArr, LockListCount);
  for i := 0 to LockListCount - 1 do
{$IFDEF FREEONTERMINATE}
    Win32Check(DuplicateHandle(GetCurrentProcess, THandle(LockList[i]),
            GetCurrentProcess, @ThreadHandleArr[i], SYNCHRONIZE, True, 0));
{$ELSE}
    ThreadHandleArr[i] := THandle(LockList[i]);
{$ENDIF}
  TTestThread.ThreadList.UnlockList;

  DeferThreadEnd.SetEvent;
  if LockListCount > 0 then begin
    Writeln('waiting for ', LockListCount, ' threads');
    WaitRet := WaitForMultipleObjects(LockListCount,
                              PWOHandleArray(ThreadHandleArr), True, INFINITE);
    case WaitRet of
      WAIT_OBJECT_0: Writeln('wait success');
      WAIT_FAILED: Writeln('wait fail:', SysErrorMessage(GetLastError));
    end;
  end;

  for i := 0 to Length(ThreadList) - 1 do begin
{$IFDEF FREEONTERMINATE}
    Win32Check(CloseHandle(ThreadHandleArr[i]));
{$ELSE}
    ThreadList[i].Free;
{$ENDIF}
  end;
  DeferThreadEnd.Free;
  TTestThread.ThreadList.Free;
  Writeln('program end');
  Readln;
end.

Ответ 1

В документации WaitForMultipleObjects() указано:

Максимальное количество дескрипторов объектов - MAXIMUM_WAIT_OBJECTS.

Значение MAXIMUM_WAIT_OBJECTS равно 64 (определено в winnt.h), поэтому 100 дескрипторов превышают лимит. Однако в документации также объясняется, как преодолеть этот предел:

Чтобы подождать более чем MAXIMUM_WAIT_OBJECTS, используйте один из следующих способов:

  • Создайте поток, чтобы ждать MAXIMUM_WAIT_OBJECTS, а затем ждите этот поток и другие дескрипторы. Используйте этот метод, чтобы разбить дескрипторы на группы MAXIMUM_WAIT_OBJECTS.

  • Вызовите RegisterWaitForSingleObject, чтобы ждать каждого дескриптора. Ожидающий поток из пула потоков ожидает MAXIMUM_WAIT_OBJECTS зарегистрированных объектов и назначает рабочий поток после того, как объект будет сигнализирован или истечет интервал времени ожидания.

См. ответ на этот вопрос для примера первой техники.