在销毁Indy组件时精确定位EInvalidPointer异常的原因

时间:2015-11-30 02:47:15

标签: delphi indy

我试图弄清楚如何解决在销毁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)并自行下载。这为竞争条件提供了触发器。

FormDestroy和Active:= False(小转移)

当我销毁表单时,我知道我需要在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组件的方式出错,如果是这样,我做错了什么以及如何解决?

更新:MCVE

以下程序使用包含Indy组件的Delphi 10 Seattle(发行版)在我的机器上合理快速地解决问题。因为这似乎是线程竞争条件,YMMV在不同的硬件上;您可能会发现单个核心VM有助于重现问题。您必须在调试器中运行以捕获EInvalidPointer异常,否则将以静默方式处理。

测试时,我很少见到EAccessViolation,并且挂起的主线程调用堆栈与EInvalidPointer相同。我怀疑这些可能都有相同的根本原因。

TIdHTTPServerTTimerTLabelTWebBrowser添加到新表单,并附加事件TIdHTTPServer.OnCommandGetTTimer.OnTimer和{ {1}}如下。设置Form.OnCreateTIdHTTPServer.Bindings[0]=127.0.0.1,9999KeepAlive=True会在我的计算机上更快地触发问题,但KeepAlive=True仍然会发生这种情况。

在Delphi调试器中运行程序时,通常会在几分钟内收到KeepAlive=False。您通常可以通过EInvalidPointer上的断点来更快地触发它,并在调试器中断时继续。

TIdThread.Cleanup

1 个答案:

答案 0 :(得分:0)

我的回答在这里耽搁了很长时间,因为我在调查一个无关的种族时再次发现了这个久违的问题。

Indy修订版5518中TIdThread.Cleanup()的补丁已解决了此竞争情况。