Это ошибка в System.Net.HttpClient на Рио? - PullRequest
1 голос
/ 25 апреля 2019

Эта функция найдена в 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;

1 Ответ

2 голосов
/ 26 апреля 2019

Целью ручного подсчета ссылок для компилятора ARC в приведенном выше коде является моделирование словаря со слабыми ссылками.Универсальные коллекции Delphi поддерживаются универсальными массивами, которые будут содержать строгую ссылку на любой объект, добавленный в коллекцию на компиляторе ARC.

Существует несколько способов получения слабых ссылок - с помощью указателей, с помощью оберток вокруг объекта, где объявлен объекткак слабый и ручной подсчет ссылок в соответствующих местах.

С указателями вы теряете безопасность типов, оболочкам требуется значительно больше кода, поэтому я предполагаю, что автор приведенного выше кода выбрал ручной подсчет ссылок.Ничего плохого в этой части.

Однако, как вы заметили, в этом коде есть что-то подозрительное - хотя подпрограмма SetExt написана правильно, RemoveExt имеет ошибку, которая впоследствии приводит к сбою.

Давайте рассмотрим код в контексте на компиляторе ARC (для краткости я опущу директивы компилятора и несвязанный код):

Поскольку добавление объекта в коллекцию (массив) увеличивает количество ссылок, чтобы добиться слабогоссылка, мы должны уменьшить количество ссылок на добавленный экземпляр объекта - таким образом, счетчик ссылок экземпляра останется неизменным после того, как он будет сохранен в коллекции.Затем, когда мы удаляем объект из такой коллекции, мы должны восстановить баланс счетчика ссылок и увеличить счетчик ссылок.Также мы должны убедиться, что объект будет удален из такой коллекции, прежде чем он будет уничтожен - хорошее место для этого - деструктор.

Добавление в коллекцию:

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

Мы добавляем объект в коллекцию, а затем после того, как коллекция содержит сильную ссылку на наш объект, мы можем освободить его счетчик ссылок.Если объект уже находится внутри коллекции, это означает, что его счетчик ссылок уже был уменьшен, и мы не должны уменьшать его снова - это цель LRelease flag.

Удаление из коллекции:

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, являются SetExt и RemoveExt методами
  2. SetExt метод является правильным
  3. Сломанный RemoveExt методкоторый не вызывает __ObjAddRef, вызывается только в THTTPClient деструкторе, и именно здесь возникает эта незначительная ошибка.

Когда деструктор вызывается для любого конкретного экземпляра объекта, что означаетЭкземпляр объекта достиг своего времени жизни, и любые последующие триггеры подсчета ссылок (во время выполнения деструктора) не влияют на правильность кода.

Это обеспечивается применением переменной objDestroyingFlag к FRefCount, изменяющей ее значение и любое дальнейшее увеличение / уменьшение счетчика больше не может привести к специальному значению 0, которое запускает процесс уничтожения - таким образом, объект безопасен и будетне быть уничтоженным дважды.

В приведенном выше коде, когда вызывается деструктор THTTPClient, это означает, что последняя сильная ссылка на экземпляр объекта вышла из области видимости или была установлена ​​на nil, и в этот момент единственной оставшейся действующей ссылкой, которая может вызвать механизм подсчета ссылок, является один в FHTTPClientList. Эта ссылка была очищена методом RemoveExt (сломан или нет) в тот момент, как было сказано ранее, это не имеет значения. И все отлично работает.

Но автор кода забыл одну крошечную причудливую вещь - DisposeOf метод, который вызывает деструктор, но в тот момент экземпляр объекта не достиг своего времени подсчета ссылок. Другими словами - если деструктор вызывается с помощью DisposeOf, любые последующие триггеры подсчета ссылок должны быть сбалансированы , поскольку все еще существуют действующие ссылки на объект, которые будут запускать механизм подсчета ссылок после завершения цепочек вызовов деструктора. Если мы прервем подсчет в этот момент, результат будет катастрофическим.

Поскольку THTTPClient не является TComponent потомком, для которого требуется DisposeOf, можно легко упустить возможность и забыть, что кто-то где-то может вызвать DipsoseOf для такой переменной в любом случае - например, если Вы создаете собственный список THTTPClient экземпляров, и очистка такого списка вызовет для них DisposeOf и с радостью нарушит их счетчик ссылок, потому что метод RemoveExt в конечном счете сломан.

Вывод: да, это БАГ.

...