这是 Rio System.Net.HttpClient 中的错误吗?

Is this a bug in System.Net.HttpClient on Rio?

这是在 Delphi Rio in 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;

上面代码中ARC编译器手动引用计数的目的是模拟弱引用字典。 Delphi 泛型 collection 由泛型数组支持,这些数组将保留对添加到 ARC 编译器 collection 的任何 object 的强引用。

有几种方法可以实现弱引用——使用指针,使用 object 周围的包装器,其中 object 被声明为弱引用,并在适当的地方进行手动引用计数。

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

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

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

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

加入collection:

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

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

从 collection 中删除:

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

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

确保object在销毁时不在列表中:

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

注意:为了让这样的faked弱collection正常工作,必须通过上面的方式添加和删除项目负责平衡引用计数的方法。使用任何其他原始 collection 方法,如 Clear 将导致引用计数损坏。


有没有bug?

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

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

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

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

这是通过在 FRefCount 变量上应用 objDestroyingFlag 来改变它的值来确保的,任何进一步的计数 increasing/decreasing 都不会再产生启动销毁过程的特殊值 0 - 所以 object 是安全的,不会被摧毁两次。

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

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

因为 THTTPClient 不是 TComponent 后代 需要 DisposeOf 很容易造成疏忽并忘记某人,某处可能无论如何都要在这样的变量上调用 DipsoseOf - 例如,如果你创建了 THTTPClient 个实例的拥有列表,清除这样的列表将调用DisposeOf 并愉快地破坏它们的引用计数,因为 RemoveExt 方法最终被破坏了。

结论:是的,是个BUG