我试图弄清楚如何解决在销毁Indy组件时出现的竞争条件。当我们在表单上有一个indy EInvalidPointer
组件时,我们在表单销毁期间收到零星的TIdHTTPServer
例外。
我们有一个简单的表单,其中包含TIdHTTPServer
和一个OnCommandGet
个活动:
procedure TForm2.httpCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
Filename: string;
function RespondFile: Boolean;
var
EnableTransferFile: Boolean;
SourceFilename: string;
begin
SourceFilename := CSourcePath + Filename;
if not FileExists(SourceFilename) then
Exit(False);
AResponseInfo.ContentType := http.MIMETable.GetFileMIMEType(SourceFilename);
AResponseInfo.ContentLength := FileSizeByName(SourceFilename);
AResponseInfo.WriteHeader;
EnableTransferFile := not (AContext.Connection.IOHandler is TIdSSLIOHandlerSocketBase);
AContext.Connection.IOHandler.WriteFile(SourceFilename, EnableTransferFile);
Result := True;
end;
begin
Filename := ARequestInfo.Document;
if Copy(Filename, 1, 1) = '/' then Delete(Filename, 1, 1);
if Filename.Contains('/') or Filename.Contains('\') then
begin
// Block path attempts
AResponseInfo.ResponseNo := 404;
Exit;
end;
if not RespondFile then
AResponseInfo.ResponseNo := 404;
end;
http服务器在设计时设置为Active=True
。
该程序提供了一个简单的网页,可以反复联系网络服务器(通过XmlHttpRequest
)并自行下载。这为竞争条件提供了触发器。
当我销毁表单时,我知道我需要在Active:=False
事件中设置FormDestroy
,否则会在Indy线程上调用OnCommandGet
事件,而不是主窗口,可以在窗体被销毁时调用,窗体成员和组件处于不确定状态。我们无法在回调的入口处测试表单的csDestroying
状态,因为这可以随时设置,即使我们的事件正在运行。
如果我们不在回调中引用表单类的任何成员,则此竞争条件不会真正导致问题。但实际上,我们经常需要访问表单的成员(使用合适的锁),因此在Active:=False
事件中设置FormDestroy
意味着我们可以控制何时拆除侦听器线程。到目前为止一切都很好。
procedure TForm2.FormDestroy(Sender: TObject);
begin
http.Active := False;
end;
但是,我们在销毁表单时偶尔会遇到EInvalidPointer
异常。这出现在httpScheduler用户线程的TIdYarnOfThread.Destroy
中:
:7775d8a8 KERNELBASE.RaiseException + 0x48
System.TObject.FreeInstance
System.ErrorAt(2,$407841)
System.Error(reInvalidPtr)
System.TObject.FreeInstance
System._ClassDestroy(???)
IdSchedulerOfThread.TIdYarnOfThread.Destroy
System.TObject.Free
IdThread.TIdThread.Cleanup
IdThread.TIdThread.Execute
System.Classes.ThreadProc($2DD5000)
System.ThreadWrapper($2E510C0)
:75c938f4 KERNEL32.BaseThreadInitThunk + 0x24
:77a65663 ;
:77a6562e ;
相应的主线程堆栈是:
:77a76fec ntdll.NtDelayExecution + 0xc
:7775a4ef KERNELBASE.Sleep + 0xf
IdGlobal.IndySleep(???)
IdScheduler.TIdScheduler.TerminateAllYarns
IdCustomTCPServer.TIdCustomTCPServer.TerminateAllThreads
IdCustomTCPServer.TIdCustomTCPServer.Shutdown
IdCustomHTTPServer.TIdCustomHTTPServer.Shutdown
IdCustomTCPServer.TIdCustomTCPServer.SetActive(???)
idyracemain.TForm2.Timer1Timer($32FDCC0)
Vcl.ExtCtrls.TTimer.Timer
Vcl.ExtCtrls.TTimer.WndProc(???)
System.Classes.StdWndProc(10552024,275,1,0)
:775384e3 user32.SetManipulationInputTarget + 0x53
:77516c30 ; C:\WINDOWS\SysWOW64\user32.dll
:77516531 ; C:\WINDOWS\SysWOW64\user32.dll
:775162f0 user32.DispatchMessageW + 0x10
Vcl.Forms.TApplication.ProcessMessage(???)
:005c22b0 TApplication.ProcessMessage + $F8
注意:上面的堆栈来自压力测试,它会使用计时器强制解决此问题,因此引用TTimer
。
从EInvalidPointer
引发的System.TObject.FreeInstance
例外通常是双重免费的。我一直在调试这个,但还不熟悉TIdYarn
以了解它的完整生命周期。但是,以下程序可能是原因:
procedure TIdSchedulerOfThread.TerminateYarn(AYarn: TIdYarn);
var
LYarn: TIdYarnOfThread;
begin
Assert(AYarn<>nil);
LYarn := TIdYarnOfThread(AYarn);
if (LYarn.Thread <> nil) and (not LYarn.Thread.Suspended) then begin
// Is still running and will free itself
LYarn.Thread.Stop;
// Dont free the yarn. The thread frees it (IdThread.pas)
end else
begin
// If suspended, was created but never started
// ie waiting on connection accept
// RLebeau: free the yarn here as well. This allows TIdSchedulerOfThreadPool
// to put the suspended thread, if present, back in the pool.
IdDisposeAndNil(LYarn);
end;
end;
这里的问题是这个过程在拥有组件的线程上下文中运行(通常是主线程)。在程序测试线程是否被挂起(即尚未启动)之后,线程可以从另一个线程启动,导致LYarn
对象被释放两次 - 一次由此函数释放,一次由其拥有的线程释放。
但我可能没有正确的结束。这种异常是由于我使用TIdHttpServer
组件的方式出错,如果是这样,我做错了什么以及如何解决?
以下程序使用包含Indy组件的Delphi 10 Seattle(发行版)在我的机器上合理快速地解决问题。因为这似乎是线程竞争条件,YMMV在不同的硬件上;您可能会发现单个核心VM有助于重现问题。您必须在调试器中运行以捕获EInvalidPointer
异常,否则将以静默方式处理。
测试时,我很少见到EAccessViolation
,并且挂起的主线程调用堆栈与EInvalidPointer
相同。我怀疑这些可能都有相同的根本原因。
将TIdHTTPServer
,TTimer
,TLabel
和TWebBrowser
添加到新表单,并附加事件TIdHTTPServer.OnCommandGet
,TTimer.OnTimer
和{ {1}}如下。设置Form.OnCreate
和TIdHTTPServer.Bindings[0]=127.0.0.1,9999
。 KeepAlive=True
会在我的计算机上更快地触发问题,但KeepAlive=True
仍然会发生这种情况。
在Delphi调试器中运行程序时,通常会在几分钟内收到KeepAlive=False
。您通常可以通过EInvalidPointer
上的断点来更快地触发它,并在调试器中断时继续。
TIdThread.Cleanup
答案 0 :(得分:0)
我的回答在这里耽搁了很长时间,因为我在调查一个无关的种族时再次发现了这个久违的问题。
Indy修订版5518中TIdThread.Cleanup()
的补丁已解决了此竞争情况。