【问题标题】:Access violation on recursive function with interfaces带有接口的递归函数的访问冲突
【发布时间】:2014-03-19 22:33:52
【问题描述】:

我正在尝试解决这个问题。这很奇怪,因为它不会引发堆栈溢出错误,而是引发访问冲突错误。 (见下面的代码。)

每当调用CallDestructor 函数时,都会调用DestroyChildren。所以它是一个递归函数。

当我只处理几个对象时,它工作得很好。我的麻烦是当我有很多实例要销毁时。

unit AggregationObject;

interface

uses
  System.Classes, System.Generics.Collections, System.Contnrs;

type
  IParentObject = Interface;

  IChildObject = Interface
    ['{061A8518-0B3A-4A1C-AA3A-4F42B81FB4B5}']
    procedure CallDestructor();
    procedure ChangeParent(Parent: IParentObject);
  End;

  IParentObject = Interface
    ['{86162E3B-6A82-4198-AD5B-77C4623481CB}']
    procedure AddChild(ChildObject: IChildObject);
    function  RemoveChild(ChildObject: IChildObject): Integer;
    function  ChildrenCount(): Integer;
    procedure DestroyChildren();
  End;

  TName = type String;
  TChildObject = class(TInterfacedPersistent, IChildObject)
    protected
      FParentObject: IParentObject;
    public
      constructor Create( AParent: IParentObject ); virtual;

      {IChildObject}
      procedure CallDestructor();
      procedure ChangeParent(Parent: IParentObject);
  end;

  TParentObject = class(TInterfacedPersistent, IParentObject)
    strict private
      FChildren: TInterfaceList;
    private
      FName: TName;
    public
      constructor Create();

      {Polimórficos}
      procedure BeforeDestruction; override;

      {IParentObject}
      procedure AddChild(AChildObject: IChildObject);
      function  RemoveChild(AChildObject: IChildObject): Integer;
      function  ChildrenCount(): Integer;
      procedure DestroyChildren();

      property Name: TName read FName write FName;
  end;

  TAggregationObject = class(TChildObject, IParentObject)
    private
      FController: IParentObject;
      function GetController: IParentObject;
    public
      constructor Create( AParent: IParentObject ); override;
      destructor Destroy(); override;

    {Controller implementation}
    public
      property Controller: IParentObject read GetController implements IParentObject;
  end;

implementation

uses
  System.SysUtils, Exceptions;

{ TChildObject }

procedure TChildObject.CallDestructor;
begin
  Self.Free;
end;

procedure TChildObject.ChangeParent(Parent: IParentObject);
begin
  if Self.FParentObject <> nil then
    IParentObject( Self.FParentObject ).RemoveChild( Self );

  Self.FParentObject := Parent;
  if Parent <> nil then
    Parent.AddChild( Self );
end;

constructor TChildObject.Create(AParent: IParentObject);
begin
  if not (AParent = nil) then
  begin
    FParentObject := AParent;
    FParentObject.AddChild( Self );
  end;
end;

{ TParentObject }

procedure TParentObject.AddChild(AChildObject: IChildObject);
begin
  if (FChildren = nil) then FChildren := TInterfaceList.Create();
    FChildren.Add( AChildObject );
end;

procedure TParentObject.BeforeDestruction;
begin
  inherited;
  DestroyChildren();
end;

function TParentObject.ChildrenCount: Integer;
begin
  Result := -1;
  if Assigned(FChildren) then
    Result := FChildren.Count;
end;

constructor TParentObject.Create;
begin
  FName := 'NoName';
end;

procedure TParentObject.DestroyChildren;
var
  Instance: IChildObject;
begin
  while FChildren <> nil do
  begin
    Instance := FChildren.Last as IChildObject;
    if Instance <> nil then
    begin
      if RemoveChild( Instance ) > -1 then
      begin
        try
          Instance.CallDestructor();
        except on E: Exception do
          raise EChildAlReadyDestroyed.Create('Parent: ' + Self.FName + #13#10 + E.Message);
        end;
      end;
    end;
  end;
end;

function TParentObject.RemoveChild(AChildObject: IChildObject): Integer;
begin
  Result := -1;{if has no children}
  if (FChildren <> nil) then
  begin

    Result := 0;{ Index 0}
    if ( ( FChildren.Items[0] as IChildObject) = AChildObject) then
      FChildren.Delete(0)
    else
      Result := FChildren.RemoveItem( AChildObject, TList.TDirection.FromEnd );

    if (FChildren.Count = 0) then
    begin
      FreeAndNil( FChildren );
    end;
  end;
end;

{ TAggregationObject }

constructor TAggregationObject.Create(AParent: IParentObject);
begin
  inherited Create(AParent);
  FController := TParentObject.Create();
  ( FController as TParentObject ).Name := Self.ClassName + '_Parent';
end;

destructor TAggregationObject.Destroy;
begin
  ( FController as TParentObject ).Free;
  inherited;
end;

function TAggregationObject.GetController: IParentObject;
begin
  Result := FController;
end;

end.

【问题讨论】:

  • FChildren 在哪里设置为零? - 它必须在某个地方,否则你的循环不会终止。 FChildren.Last 是否保证始终为非 NIL?异常究竟发生在哪里?
  • 添加完整的 fastmm 调试模式并重试。
  • 我们仍然没有看到递归循环的关闭......我想提供一个提示:递归在最好的时候可能很棘手。您的间接层级太多,难以理解,也很容易出错。
  • “老兄”,您正在寻求我们的帮助,因为自己无法解决它....很可能因为你把注意力集中在了错误的地方。如果您对整个调用链是否相关的意见是 100% 可靠的 - 您将不需要我们的帮助!
  • 我改变了我的结构。我认为问题在于混合对象引用和接口。甚至我的对象也不受 RefCount 控制,后台发生了一些事情:“但是,由于接口引用的性质,当引用超出范围时,_AddRef 和 _Release 仍将被调用。如果在此之前已释放类时间,那么你在 _IntfClear 中有一个 AV。”我在堆栈中的最后一次调用是 _IntfClear 或 _IntfCopy。我认为这是问题所在。我不确定如何纠正它,所以我改成了一个抽象类。谢谢,@AndersE.Andersen

标签: delphi recursion interface access-violation


【解决方案1】:

细节就是区别。

TValueObject 是 TAggregationObject 的一个特化,它实现了 IMasterValue,如下所示:

IMasterValue = interface
  //GUID Here
  function MasterValue: variant;
end;

TValueObject = class(TAggregationObject , IMasterValue)
public
  function MasterValue: variant;
end;

所以我有: TSomeService = 类 民众 函数查找(AMasterValue:IMasterValue):TValueObject; 结束;

procedure DoSome(AValueObject: TValueObject);
begin
with TSomeService.Create() do
  begin
    try
      Find(AValueObject); //This will get cleared when method exits
    finally
      AValueObject.Free(); //But the object is destroyed before that
    end;  
  end;
end;

//在大并发时发生,因为内存会被复用,否则内存还在隐藏问题。线程运行循环销毁会显示问题。

解决方法是:

procedure DoSome(AValueObject: TValueObject);
var
  LMasterValue: IMasterValue;
begin
  with TSomeService.Create() do
  begin
    try
      LMasterValue := AValueObject;
      try
        Find(LMasterValue);
      finally
        LMasterValue := nil;        
      end;  
    finally
      AValueObject.Free();
    end;
  end;
end;

【讨论】:

    【解决方案2】:

    OP 设法确定了问题,但尚未发布答案。我提供了他评论的编辑版本并添加了更详细的解释。

    我认为问题在于混合对象引用和接口。即使我的对象不受 RefCount 控制,后台也发生了一些事情:“但是,由于接口引用的性质,当引用超出范围时,_AddRef 和 _Release 仍将被调用。如果该类在之前被销毁那个时候,你在 _IntfClear 中有一个 AV。”我在堆栈中的最后一次调用是 _IntfClear 或 _IntfCopy。我认为这是问题所在。我不确定如何纠正它,所以我已更改为抽象类。

    访问冲突不是由混合对象引用和接口引起的;有一些方法可以安全地做到这一点。
    但它们是由于 Delphi 试图 _Release 对已被销毁的对象的引用造成的。

    然而,这就提出了一个问题:“为什么 AV 只是偶尔发生,而不是一直发生?”

    为了解释,我将讨论一个非法内存操作。我的意思是一段代码(或对象)访问它不应该访问的内存。

    每次您的程序执行非法内存操作时,您不会得到一个 AV。仅当非法内存操作注意到时才会引发 AV!有两个主要原因可能会被忽视:

    • 程序中的一个对象访问某些内存可能是“非法的”,但如果另一个实例访问该内存合法的 - 那么系统就没有办法注意到您实际上已经进行了非法内存操作
    • 大多数情况下,FastMem 以比您实际从 FastMem 请求的更大的“页面”向操作系统请求内存。然后它会跟踪页面上的多个较小的分配。只有当页面上没有更小的分配时,页面才会返回给操作系统。因此,操作系统不会注意到仍然分配给您的程序的页面上的任何非法内存操作

    上面的第二个原因是为什么少量对象不会导致 AV:分配对象的页面仍然分配给您的程序。
    但是当您有大量实例时:有时当您销毁一个对象时,它是页面上的最后一个;并将页面返回给操作系统...因此,当在该页面上调用 _Release 时,您会得到 AV。

    那么,你如何解决它?

    嗯,您选择的选项(使用抽象类而不是接口)有效。但是你失去了接口的好处。但是,我建议不要尝试手动控制界面对象的销毁。接口引用的好处之一是底层对象会自毁(如果你允许它们)。

    我怀疑你这样做是因为你混合了对象引用和接口引用。因此,与其强迫你的接口表现得像对象(这样做你已经很麻烦了),不如简单地让你的每个对象引用手动添加对接口的引用。您可以使用以下代码执行此操作:

    (ObjectRef as IUnkown)._AddRef;
    //Do stuff with ObjectRef
    (ObjectRef as IUnkown)._Release;
    

    旁注:
    您发现没有引发 Stack Overflow 错误很奇怪。 (很明显,你知道为什么会提出 AV。)我想指出,通常递归只会触发 SO 错误:如果递归非常深(我的意思是 非常);或者如果每次递归都在堆栈上分配了相当多的内存。

    【讨论】:

    • 这是一个真正解释清楚的答案。我感谢你的帮助。基于我的框架,现在处理它会很困难,但我会做一些测试。非常感谢您的关注。
    猜你喜欢
    • 1970-01-01
    • 2012-09-04
    • 2023-03-16
    • 2020-12-13
    • 1970-01-01
    • 2013-11-20
    • 1970-01-01
    • 2015-01-26
    • 1970-01-01
    相关资源
    最近更新 更多