这是 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
是私有变量,在任何其他方式。乍一看,该代码可以正常工作,但实际上包含相当微妙的错误。
为了解决真正的错误,我们需要涵盖可能的使用场景,从几个已确定的事实开始:
- 只有
FHTTPClientList
字典中项目的引用计数改变内容的方法是 SetExt
和 RemoveExt
方法
SetExt
方法正确
- 不调用
__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
这是在 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
是私有变量,在任何其他方式。乍一看,该代码可以正常工作,但实际上包含相当微妙的错误。
为了解决真正的错误,我们需要涵盖可能的使用场景,从几个已确定的事实开始:
- 只有
FHTTPClientList
字典中项目的引用计数改变内容的方法是SetExt
和RemoveExt
方法 SetExt
方法正确- 不调用
__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