首页 > 解决方案 > 这是 Rio 上 System.Net.HttpClient 中的错误吗?

问题描述

这是在 Delphi Rio 中找到的功能System.Net.HttpClient

THTTPClientHelper = class helper for THTTPClient
....

procedure THTTPClientHelper.SetExt(const Value);
var
{$IFDEF AUTOREFCOUNT}
  LRelease: Boolean;
{$ENDIF}
  LExt: THTTPClientExt;
begin
  if FHTTPClientList = nil then
    Exit;
  TMonitor.Enter(FHTTPClientList);
  try
{$IFDEF AUTOREFCOUNT}
    LRelease := not FHTTPClientList.ContainsKey(Self);
{$ENDIF}
    LExt := THTTPClientExt(Value);
    FHTTPClientList.AddOrSetValue(Self, LExt);
{$IFDEF AUTOREFCOUNT}
    if LRelease then __ObjRelease;
{$ENDIF}
  finally
    TMonitor.Exit(FHTTPClientList);
  end;
end;

这家伙想在LRelease这里做什么?

{$IFDEF AUTOREFCOUNT}
    LRelease := not FHTTPClientList.ContainsKey(Self);
{$ENDIF}
    LExt := THTTPClientExt(Value);
    FHTTPClientList.AddOrSetValue(Self, LExt);
{$IFDEF AUTOREFCOUNT}
    if LRelease then __ObjRelease;
{$ENDIF}

因此,如果FHTTPClientList不包含将其THTTPClient添加到 中FHTTPClientList,然后将其引用计数减少一。为什么将它的引用计数减少一个?THTTPClient仍然活着并使用了为什么要打破它的引用计数?他们是这里的一个错误,也许那个人打错了,但我不明白他最初想做什么......

有关如何从字典中删除项目的信息:

procedure THTTPClientHelper.RemoveExt;
begin
  if FHTTPClientList = nil then
    Exit;
  TMonitor.Enter(FHTTPClientList);
  try
    FHTTPClientList.Remove(Self);
  finally
    TMonitor.Exit(FHTTPClientList);
  end;
end;

标签: delphifiremonkeydelphi-10.3-rio

解决方案


上述代码中 ARC 编译器的手动引用计数的目的是模拟具有弱引用的字典。Delphi 泛型集合由泛型数组支持,该数组将持有对添加到 ARC 编译器集合中的任何对象的强引用。

有几种方法可以实现弱引用 - 使用指针,在对象被声明为弱引用的对象周围使用包装器,并在适当的位置手动引用计数。

使用指针会失去类型安全性,包装器需要更多的代码,所以我猜上述代码的作者选择了手动引用计数。那部分没有错。

但是,正如您所注意到的,该代码中有一些可疑之处 - 虽然SetExt正确编写了例程,但RemoveExt有一个错误导致稍后崩溃。

让我们在 ARC 编译器的上下文中查看代码(为简洁起见,我将省略编译器指令和不相关的代码):

由于将对象添加到集合(数组)中会增加引用计数,为了实现弱引用,我们必须减少添加的对象实例的引用计数——这样实例的引用计数在存储到集合后将保持不变。接下来,当我们从此类集合中删除对象时,我们必须恢复引用计数平衡并增加引用计数。此外,我们必须确保对象在销毁之前从此类集合中删除 - 这样做的好地方是析构函数。

添加到收藏:

LRelease := not FHTTPClientList.ContainsKey(Self);
FHTTPClientList.AddOrSetValue(Self, LExt);
if LRelease then __ObjRelease;

我们将对象添加到集合中,然后在集合持有对我们对象的强引用后,我们可以释放它的引用计数。如果对象已经在集合中,这意味着它的引用计数已经减少了,我们不能再减少它——这就是LRelease标志的目的。

从集合中移除:

if FHTTPClientList.ContainsKey(Self) then
  begin
    __ObjAddRef;
    FHTTPClientList.Remove(Self);
  end;

如果对象在集合中,我们必须在从集合中删除对象之前恢复平衡并增加引用计数。这是RemoveExt方法中缺少的部分。

确保对象在销毁时不在列表中:

destructor THTTPClient.Destroy;
begin
  RemoveExt;
  inherited;
end;

注意:为了使此类伪造的弱集合正常工作,必须仅通过上述负责平衡引用计数的方法添加和删除项目。使用任何其他原始收集方法Clear都会导致引用计数损坏。


错误与否?

System.Net.HttpClient代码中,破坏 RemoveExt方法仅在析构函数中调用,也是FHTTPClientList私有变量,不会以任何其他方式更改。乍一看,该代码可以正常工作,但实际上包含相当微妙的错误。

为了揭示真正的错误,我们需要涵盖可能的使用场景,从几个既定事实开始:

  1. 只有改变内容和FHTTPClientList字典中项目的引用计数的方法才是SetExtRemoveExt方法
  2. SetExt方法是正确的
  3. RemoveExt不调用的损坏方法__ObjAddRef仅在THTTPClient析构函数中调用,这就是这个微妙错误的来源。

当对任何特定对象实例调用析构函数时,这意味着对象实例已达到其生命周期,并且任何后续引用计数触发器(在析构函数执行期间)都不会影响代码的正确性。

这是通过应用变量更改其值来确保的objDestroyingFlagFRefCount并且任何进一步的计数增加/减少都不会再导致0启动销毁过程的特殊值 - 因此对象是安全的并且不会被销毁两次。

在上面的代码中,当THTTPClient调用析构函数时,这意味着对对象实例的最后一个强引用已经超出范围或被设置为nil,此时唯一剩余的可以触发引用计数机制的活动引用是FHTTPClientList. 如前所述,该引用已通过RemoveExt方法(是否损坏)清除,这无关紧要。一切正常。

但是,代码的作者忘记了一个很小的事情——DisposeOf触发析构函数的方法,但此时对象实例还没有达到它的引用计数生命周期。换句话说 - 如果析构函数由 调用DisposeOf,任何后续引用计数触发器都必须平衡,因为在析构函数链调用完成后,仍然存在对将触发引用计数机制的对象的实时引用。如果我们在那个时候打破计数,结果将是灾难性的。

由于THTTPClient不是TComponent后代,因此 DisposeOf很容易进行疏忽并忘记某人,某处DipsoseOf无论如何都可以调用这样的变量 - 例如,如果您制作拥有的THTTPClient实例列表,清除此类列表将调用DisposeOf它们并愉快地打破它们的引用计数,因为RemoveExt方法最终被打破。

结论:是的,这是一个BUG。


推荐阅读