Delphi: убить потоки, когда приложение завершает работу?
фон: мне нужно выполнить проверки, доступна ли куча сетевых дисков или удаленных компьютеров. С каждого DirectoryExists()
требуется много времени до потенциального таймаута, я выполняю проверки в отдельных потоках. Может случиться так, что конечный пользователь закрывает приложение, пока некоторые проверки все еще выполняются. С DirectoryExists()
блоки, у меня нет шансов использовать классический while not Terminated
подход.
procedure TMyThread.Execute;
begin
AExists := DirectoryExists(AFilepath);
end;
Вопрос 1: Проблема в том, что некоторые потоки все еще запуск при завершении работы приложения? Будет ли Windows просто убирать за мной, и все? Внутри IDE я получаю уведомление о несвободных объектах, но вне IDE это просто кажется мирным.
Вопрос 2: Можно ли прекратить такие простые потоки с помощью TerminateThread
или это потенциально вредно в этом случае?
Вопрос 3: я обычно беру результаты из нитей в OnTerminate()
событие и пусть темы FreeOnTerminate
далее. Если бы я хотел освободить их сам, когда я должен это сделать? Могу ли я освободить нить в его OnTerminate
событие или это немного слишком рано? Как бы поток сообщил мне, что это делается, если не с OnTerminate
?
2 ответов
проблема в том, что некоторые потоки все еще работают, когда приложение завершает работу?
если понимать буквально, этот вопрос немного искажен. Это потому, что после , как по умолчанию завершается приложение Delphi, потоки не выполняются.
ответ на вопрос "это проблема, что некоторые потоки не имели возможности закончить" зависит от того, что эти потоки не удалось завершить. Вам придется тщательно анализировать код потока, но в целом это может быть подвержено ошибкам.
будет ли Windows просто убирать за мной, и все? Внутри IDE я получаю уведомление о несвободных объектах, но вне IDE он просто появляется быть мирной.
ОС восстановит выделенную память, когда адресное пространство процесса будет уничтожено, все дескрипторы объектов будут закрыты, когда таблица дескриптора процесса уничтожается, точки входа всех загруженных библиотек будут вызываться с помощью DLL_PROCESS_DETACH
. Я не могу найти никакой документации по этому поводу, но я также предполагаю, что ожидающие запросы IO будут вызваны для отмены.
но все это не означает, что не будет никаких проблем. Вещи могут стать беспорядочными, например, с участием межпроцессных коммуникаций или объектов синхронизации. документация на ExitProcess
подробно один такой пример: если поток исчезает перед выпуском заблокируйте, что одна из библиотек пытается получить при отсоединении, есть тупик. этой сообщение в блоге дает еще один конкретный пример, когда процесс выхода принудительно завершается ОС, если поток пытается войти в критический раздел, который осиротел другим уже завершенным потоком.
хотя может иметь смысл отпустить высвобождение ресурсов во время выхода, особенно если очистка занимает значительное количество времени, можно ошибиться для нетривиального приложения. Надежная стратегия заключается в том, чтобы очистить все до ExitProcess
называется. OTOH, если вы окажетесь в ситуации, когда ExitProcess
уже вызван, например, процесс отсоединяется от вашей dll из - за завершения, почти единственная безопасная вещь-это оставить все позади и вернуться-каждая другая dll уже могла быть выгружена и каждый другой поток завершен.
можно ли завершите такие простые потоки с помощью TerminateThread или это потенциально опасно в этом случае?
TerminateThread
рекомендуется использовать только в самые экстремальные случаи, но поскольку вопрос имеет жирный "это", что действительно делает код, следует изучить. Глядя на код RTL, мы видим, что худшее, что может произойти, - это оставить дескриптор файла открытым, к которому можно получить доступ только для чтения. Это не проблема во время завершения процесса, так как дескриптор скоро будет закрыто.
я обычно беру результаты из потоков в событии OnTerminate () и позволяю потокам FreeOnTerminate впоследствии. Если бы я хотел бесплатно когда я должен это сделать?
единственное строгое правило-после того, как они закончат выполнение. Выбор, вероятно, будет определяться дизайном приложения. Что было бы по-другому, вы не смогли бы использовать FreeOnTerminate
и вы бы сохранили ссылки на свои потоки, чтобы иметь возможность освободить их. В тестовом случае, над которым я работал для ответа на этот вопрос, рабочие потоки, которые закончены, освобождаются при срабатывании таймера, как сборщик мусора.
могу ли я освободить поток в его событии OnTerminate или это немного слишком рано?
освобождение объекта в одном из его собственных обработчиков событий вызывает риск работы на освобожденная память экземпляра. The документация специально предупреждает об этом для компонентов, но в целом это применимо ко всем классам.
даже если вы хотите игнорировать предупреждение, это тупик. Хотя обработчик вызывается после Execute
возвращает OnTerminate
по-прежнему синхронизируется с ThreadProc. Если вы попытаетесь освободить поток в обработчике, это вызовет ожидание от основного потока для завершения потока, который ждет основного потока поток для возврата из OnTerminate
, что является тупиком.
как бы поток сообщил мне, что это делается, если не с OnTerminate?
OnTerminate
отлично подходит для информирования о том, что поток выполнил свою работу, хотя вы можете использовать другие средства, такие как использование объектов синхронизации или очереди процедуры или публикации сообщения и т. д.. Также стоит отметить, что можно подождать на ручке потока, что и есть TThread.WaitFor
делает.
в моей тестовой программе я попытался определить время завершения приложения в зависимости от различных стратегий выхода. Все результаты тестирования зависят от среды тестирования.
время окончания измеряется, начиная с момента, когда OnClose
вызывается обработчик формы VCL и заканчивается непосредственно перед ExitProcess
вызывается RTL. Кроме того, этот метод не учитывает, как долго ExitProcess
берет, что я предположим, будет по-другому, когда есть болтающиеся нити. Но я все равно не пытался его измерить.
рабочие потоки запрашивают наличие каталога на несуществующем хосте. Это самое большее, что я могу сказать о времени ожидания. Каждый запрос находится на новом несуществующем хосте, иначе DirectoryExists
сразу возвращается.
таймер запускается и собирает рабочие потоки. В зависимости от времени запроса ввода-вывода (которое составляет около 550ms) интервал таймера влияет на общее количество потоков в любой момент времени. Я тестировал около 10 потоков с интервалом таймера 250 мс.
различные выходы отладки позволяют следить за потоком в журнале событий IDE.
моим первым тестом было оставить рабочие потоки позади-просто закройте приложение. Время, которое я измерил, было 30-65ms. Опять же, это могло вызвать
ExitProcess
сам, чтобы занять больше времени.далее, я тестировал завершение нити с
TerminateThread
. Это заняло 140-160ms. Я считаю, что это на самом деле ближе к тому, что предыдущий тест подошел бы, если времяExitProcess
может быть учтено. Но у меня нет доказательств.затем я протестировал отмену запроса ввода-вывода при запуске потоков, а затем оставил их позади.Это значительно уменьшило объем просочившейся памяти, фактически полностью устраненной в большинстве запусков. Хотя запрос на отмену является асинхронным, почти все нити немедленно вернуться и найти время, чтобы закончить. Во всяком случае, это заняло 160-190ms.
я должен отметить здесь, что код в DirectoryExists
неисправен, по крайней мере, в XE2. Первое, что делает функция, это вызывает GetFileAttributes
. Ан INVALID_FILE_ATTRIBUTES
return обозначает сбой функции. Вот как RTL обрабатывает сбой:
function DirectoryExists(const Directory: string; FollowLink: Boolean = True): Boolean;
...
...
Result := False;
Code := GetFileAttributes(PChar(Directory));
if Code <> INVALID_FILE_ATTRIBUTES then
begin
...
end
else
begin
LastError := GetLastError;
Result := (LastError <> ERROR_FILE_NOT_FOUND) and
(LastError <> ERROR_PATH_NOT_FOUND) and
(LastError <> ERROR_INVALID_NAME) and
(LastError <> ERROR_BAD_NETPATH);
end;
end;
этот код предполагает, что если GetLastError
возвращает один из вышеуказанных кодов ошибок, каталог существует. Это рассуждение недостатки. Действительно, при отмене НЛ запросу GetLastError
возвращает ERROR_OPERATION_ABORTED
(995), как описано, но DirectoryExists
возвращает true независимо от того, существует каталог или нет.
ожидание завершения потоков без отмены ввода-вывода занимает 330-530ms. Это полностью устраняет утечки памяти.
отмена запросов ввода-вывода, а затем ожидание завершения потоков занимает 170-200ms. Конечно, здесь тоже нет утечек памяти. Учитывая, что нет существенной разницы во времени в любом из вариантов, это будет тот, который я выберу.
код тестирования, который я использовал, приведен ниже:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes,
Vcl.Controls, Vcl.Forms, Vcl.ExtCtrls,
generics.collections;
type
TForm1 = class(TForm)
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure FormDestroy(Sender: TObject);
private
FThreads: TList<TThread>;
end;
var
Form1: TForm1;
implementation
uses
diagnostics;
{$R *.dfm}
type
TIOThread = class(TThread)
private
FTarget: string;
protected
constructor Create(Directory: string);
procedure Execute; override;
public
destructor Destroy; override;
end;
constructor TIOThread.Create(Directory: string);
begin
FTarget := Directory;
inherited Create;
end;
destructor TIOThread.Destroy;
begin
inherited;
OutputDebugString(PChar(Format('Thread %d destroyed', [ThreadID])));
end;
procedure TIOThread.Execute;
var
Watch: TStopwatch;
begin
OutputDebugString(PChar(Format('Thread Id: %d executing', [ThreadID])));
Watch := TStopwatch.StartNew;
ReturnValue := Ord(DirectoryExists(FTarget));
Watch.Stop;
OutputDebugString(PChar(Format('Thread Id: %d elapsed time: %dms, return: %d',
[ThreadID, Watch.Elapsed.Milliseconds, ReturnValue])));
end;
//-----------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
FThreads := TList<TThread>.Create;
Timer1.Interval := 250;
Timer1.Enabled := True;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FThreads.Free;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
ShareName: array [0..12] of Char;
i: Integer;
H: THandle;
begin
for i := FThreads.Count - 1 downto 0 do
if FThreads[i].Finished then begin
FThreads[i].Free;
FThreads.Delete(i);
end;
for i := Low(ShareName) to High(ShareName) do
ShareName[i] := Chr(65 + Random(26));
FThreads.Add(TIOThread.Create(Format('\%s\share', [string(ShareName)])));
OutputDebugString(PChar(Format('Possible thread count: %d', [FThreads.Count])));
end;
var
ExitWatch: TStopwatch;
// not declared in XE2
function CancelSynchronousIo(hThread: THandle): Bool; stdcall; external kernel32;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
i: Integer;
Handles: TArray<THandle>;
IOPending: Bool;
Ret: DWORD;
begin
ExitWatch := TStopwatch.StartNew;
// Exit;
Timer1.Enabled := False;
{
for i := 0 to FThreads.Count - 1 do
TerminateThread(FThreads[i].Handle, 0);
Exit;
//}
if FThreads.Count > 0 then begin
SetLength(Handles, FThreads.Count);
for i := 0 to FThreads.Count - 1 do
Handles[i] := FThreads[i].Handle;
//{
OutputDebugString(PChar(Format('Cancelling at most %d threads', [Length(Handles)])));
for i := 0 to Length(Handles) - 1 do
if GetThreadIOPendingFlag(Handles[i], IOPending) and IOPending then
CancelSynchronousIo(Handles[i]);
//}
//{
Assert(FThreads.Count <= MAXIMUM_WAIT_OBJECTS);
OutputDebugString(PChar(Format('Will wait on %d threads', [FThreads.Count])));
Ret := WaitForMultipleObjects(Length(Handles), @Handles[0], True, INFINITE);
case Ret of
WAIT_OBJECT_0: OutputDebugString('wait success');
WAIT_FAILED: OutputDebugString(PChar(SysErrorMessage(GetLastError)));
end;
//}
for i := 0 to FThreads.Count - 1 do
FThreads[i].Free;
end;
end;
procedure Exiting;
begin
ExitWatch.Stop;
OutputDebugString(PChar(
Format('Total exit time:%d', [ExitWatch.Elapsed.Milliseconds])));
end;
initialization
ReportMemoryLeaksOnShutdown := True;
ExitProcessProc := Exiting;
end.
проблема в том, что некоторые потоки все еще работают, когда приложение завершает работу?
возможно, да. Это зависит от того, что ваш код делает после DirectoryExists()
выход. Вы можете в конечном итоге попытаться получить доступ к вещам, которые больше не существуют.
будет ли Windows просто убирать за мной, и все?
чтобы убедиться, что все очищено должным образом, Вы несете ответственность за прекращение собственных потоков. Когда основной VCL поток выполняется, он будет вызывать ExitProcess()
, который принудительно завершит любые вторичные потоки, которые все еще работают, что не позволит им очистить после себя или уведомить любые загруженные библиотеки DLL, что они отсоединяются от потоков.
можно ли прекратить такие простые потоки с помощью TerminateThread или это потенциально опасно в этом случае?
TerminateThread()
всегда потенциально вредно. никогда не использовать он.
обычно я беру результаты из потоков в событии OnTerminate () и позволяю потокам FreeOnTerminate впоследствии.
это не будет работать, если главный цикл обработки сообщений до завершения потока. По умолчанию TThread.OnTerminate
событие запускается с помощью вызова TThread.Synchronize()
. Как только основной цикл сообщений перестанет работать, не будет ничего для обработки ожидающего Synchronize()
запросы, если вы не Запустите свой собственный цикл при выходе из приложения, чтобы вызвать RTL CheckSynchronize()
процедура, пока все ваши потоки не будут полностью завершены.
если бы я хотел освободить их сам, когда я должен это сделать?
прежде чем ваше приложение хочет выйти.
могу ли я освободить поток в его событии OnTerminate
нет.
или это слишком рано?
это и потому, что всегда небезопасно освобождать объект внутри события, вызванного тот же самый предмет. RTL по-прежнему нуждается в доступе к объекту после выхода обработчика событий.
это, как говорится, так как у вас нет очистить способ безопасного завершения потоков, я предлагаю не разрешать вашему приложению выходить, когда потоки все еще работают. Когда пользователь запрашивает приложение для выхода, проверьте, есть ли запущенные потоки, и если да, то отобразите занятой пользовательский интерфейс для пользователя, дождитесь завершения всех потоков, а затем приложение.
например:
constructor TMyThread.Create(...);
begin
inherited Create(False);
FreeOnTerminate := True;
...
end;
procedure TMyThread.Execute;
begin
...
if Terminated then Exit;
AExists := DirectoryExists(AFilepath);
if Terminated then Exit;
...
end;
type
TMainForm = class(TForm)
...
procedure FormClose(Sender: TObject; var Action: TCloseAction);
...
private
ThreadsRunning: Integer;
procedure StartAThread;
procedure ThreadTerminated(Sender: TObject);
...
end;
...
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if ThreadsRunning = 0 then Exit;
// signal threads to terminate themselves...
if CheckWin32Version(6) then
ShutdownBlockReasonCreate(Handle, 'Waiting for Threads to Terminate');
try
// display busy UI to user ...
repeat
case MsgWaitForMultipleObjects(1, System.Classes.SyncEvent, False, INFINITE, QS_ALLINPUT) of
WAIT_OBJECT_0 : CheckSynchronize;
WAIT_OBJECT_0+1 : Application.ProcessMessages;
WAIT_FAILED : RaiseLastOSError;
end;
until ThreadsRunning = 0;
// hide busy UI ...
finally
if CheckWin32Version(6) then
ShutdownBlockReasonDestroy(Handle);
end;
end;
procedure TMainForm.StartAThread;
var
Thread: TMyThread;
begin
Thread := TMyThread.Create(...);
Thread.OnTerminate := ThreadTerminated;
Thread.Start;
Inc(ThreadsRunning);
end;
procedure TMainForm.ThreadTerminated(Sender: TObject);
begin
Dec(ThreadsRunning);
...
end;
кроме того:
type
TMainForm = class(TForm)
...
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
...
private
ThreadsRunning: Integer;
WaitingForClose: Boolean;
procedure StartAThread;
procedure ThreadTerminated(Sender: TObject);
...
end;
...
procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
CanClose := (ThreadsRunning = 0);
if CanClose or WaitingForClose then Exit;
// signal threads to terminate themselves...
WaitingForClose := True;
// display busy UI to user ...
if CheckWin32Version(6) then
ShutdownBlockReasonCreate(Handle, 'Waiting for Threads to Terminate');
end;
procedure TMainForm.StartAThread;
var
Thread: TMyThread;
begin
Thread := TMyThread.Create(...);
Thread.OnTerminate := ThreadTerminated;
Thread.Start;
Inc(ThreadsRunning);
end;
procedure TMainForm.ThreadTerminated(Sender: TObject);
begin
Dec(ThreadsRunning);
...
if WaitingForClose and (ThreadsRunning = 0) then
begin
WaitingForClose := False;
// hide busy UI ...
if CheckWin32Version(6) then
ShutdownBlockReasonDestroy(Handle);
Close;
end;
end;