W poniższym programie testowym każdy wątek testowy dodaje swój uchwyt do globalnego TThreadList
, gdy rozpoczyna wykonywanie, i usuwa jego uchwyt z tej samej listy, gdy jego zakończenie ma się wkrótce zakończyć.Dlaczego funkcja WaitForMultipleObjects kończy się niepowodzeniem z wieloma uchwytami wątków?
Dla celów testowania dodatkowo każdy wątek upewnia się, że dodaje swój uchwyt, zanim główny wątek zablokuje listę (aby powielić swoje uchwyty i zacząć na nich czekać, aby zakończyć). Gwinty upewniają się również, że nie usuwają uchwytów, zanim główny wątek nie zablokuje listy.
Program testowy działa poprawnie dla około 50-60 wątków. Po tym, połączenie WaitForMultipleObjects
zaczyna się niepowodzeniem z WAIT_FAILED
, GetLastError
zwraca 87 (ERROR_INVALID_PARAMETER
). Obecnie rozpoczyna 100 wątków. Moje pytanie brzmi: co robię źle?
Program jest kompilowany za pomocą XE2 - aktualizacja 4, 32-bitowa platforma docelowa. Pole testowe to 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.
Istnieje również możliwość użycia wariantu [tego podejścia] (http://tondrej.blogspot.com/2016/03/the-strange-limitation-of-64-threads.html): interlocked-incremented/dekrementowany licznik i pojedyncze wydarzenie, na które trzeba czekać. –
Dzięki za edycję. Czuję się trochę źle biorąc kredyt za lepszą odpowiedź niż moja :-) –
Należy również zauważyć, że limit 64 obiektów jest w rzeczywistości ograniczeniem po stronie jądra. Przedefiniowanie MAXIMUM_WAIT_OBJECTS lub nawet ponowne wprowadzenie strony użytkownika WaitForMultipleObjects nie przyniesie wiele. Trzeba po prostu obejść ten konkretny limit. –