Delphi: агрегирование объектов и утечки памяти с использованием атрибута [weak]

Я хочу создать класс TParent, содержащий несколько дочерних объектов, с помощью агрегирования. Некоторые объекты независимы, а некоторые могут зависеть и от других дочерних элементов. Все дочерние объекты должны иметь ссылку на родительский. Я также хочу использовать интерфейсы, где это возможно.

Для этой цели я использую TInterfacedObject для TParent и TAggregatedObject для детей. Поскольку и ребенок, и родитель знают друг о друге, я использую слабые ссылки, чтобы избежать цепной зависимости. Фактически это поведение уже определено в TAggregatedObject. Все работает нормально, когда я использую только независимые дочерние объекты (TIndependantChild).

Проблема возникает, когда дочерний объект зависит также от других дочерних объектов, см. Конструктор TDependantChild. Я сохраняю ссылку на другой дочерний объект в переменной fChild, которая помечена атрибутом [weak], введенным в Delphi 10 Berlin. FastMM4 сообщает об утечках памяти при выключении:

введите описание изображения здесь

Также возникает нарушение прав доступа, приводящее к System.TMonitor.Destroy, но это происходит только тогда, когда FastMM4 используется и ReportMemoryLeaksOnShutDown имеет значение True.

program Project1;

{$APPTYPE CONSOLE}

uses
  FastMM4,
  System.SysUtils;

type
  IParent = interface
  ['{B11AF925-C62A-4998-855B-268937EF30FB}']
  end;

  IChild = interface
  ['{15C19A4E-3FF2-4639-8957-F28F0F44F8B4}']
  end;

  TIndependantChild = class(TAggregatedObject, IChild)
  end;

  TDependantChild = class(TAggregatedObject, IChild)
  private
    [weak] fChild: IChild;
  public
    constructor Create(const Controller: IInterface; const AChild: IChild); reintroduce;
  end;

  TParent = class(TInterfacedObject, IParent)
  private
    fIndependantChild: TIndependantChild;
    fDependantChild: TDependantChild;
  public
    constructor Create;
    destructor Destroy; override;
  end;

{ TParent }

constructor TParent.Create;
begin
  fIndependantChild := TIndependantChild.Create(Self);
  fDependantChild := TDependantChild.Create(Self, fIndependantChild);
end;

destructor TParent.Destroy;
begin
  fDependantChild.Free;
  fIndependantChild.Free;
  inherited;
end;

{ TDependantChild }

constructor TDependantChild.Create(const Controller: IInterface; const AChild: IChild);
begin
  inherited Create(Controller);
  fChild := AChild;
end;

var
  Owner: IParent; 

begin
  ReportMemoryLeaksOnShutDown := True;
  Owner := TParent.Create;
  Owner := nil;
end.

Я обнаружил, что использование [unsafe] вместо [weak] решает проблему, но согласно delphi справка

Его ([небезопасно]) следует использовать вне системного блока только в очень редких случаях.

Поэтому я не уверен, что мне следует использовать здесь [unsafe], особенно когда я не понимаю, что происходит.

Итак, каковы причины утечек памяти в этой ситуации и как их преодолеть?


person VyPu    schedule 03.11.2017    source источник
comment
Зачем нужно объединять детей? Вы понимаете, что такое агрегация? Почему вы смешиваете объектные ссылки и интерфейсы? Это всегда путь к катастрофе. Используйте отладчик, чтобы узнать, почему происходит утечка памяти.   -  person Remy Lebeau    schedule 03.11.2017
comment
[weak] был добавлен для интерфейсов в 10.1 Berlin, но [weak] существует в более ранних версиях. Я могу скомпилировать ваш код как есть в XE2, но [weak] не действует. Owner имеет RefCount=1, потому что TDependantChild имеет неслабую ссылку на TParent (из-за агрегации) при назначении fChild, несмотря на то, что он [weak]. Когда Owner уничтожается, TInterfacedObject.BeforeDestruction() вызывает ошибку, когда RefCount<>0, вызывая утечку Owner и его дочерних элементов. Изменение fChild на Pointer исправляет это. Подозреваю нечто подобное в вашем случае при использовании [unsafe]. Подтвердить с помощью отладчика   -  person Remy Lebeau    schedule 03.11.2017
comment
Реми, спасибо за комментарии. Я новичок в концепции агрегации, возможно, я неправильно ее понял. В моем случае дочерние объекты представляют определенную функциональность класса. Они не имеют смысла без Родителя, но одни и те же дочерние объекты могут использоваться для составления разных родителей. Мне тоже не нравилось смешивать интерфейсы и ссылки на объекты, но я нашел несколько примеров, когда это было сделано так link, и это вызывало ошибки при использовании только интерфейсов. Скоро проверю второй комментарий.   -  person VyPu    schedule 03.11.2017
comment
Я установил точку останова отладчика на procedure TInterfacedObject.BeforeDestruction. Без [weak] RefCount действительно равно 1, а с [weak] это 0. Таким образом, похоже, что это утечка где-то еще.   -  person VyPu    schedule 03.11.2017


Ответы (1)


Проблема с утечкой и сбоем при использовании внешнего диспетчера памяти FastMM4 связана со следующей проблемой, касающейся завершения внутренней HashMap, используемой для отслеживания слабых ссылок.

[REGRESSION XE2 / 10.1 Berlin] Невозможно использовать сторонние диспетчеры памяти

Из-за этой проблемы невозможно использовать сторонние диспетчеры памяти для обнаружения утечек в Delphi 10.1 и более новых версиях, включая внешний FastMM4.

Вот почему у вас проблемы с атрибутом [weak] и нет проблем с [unsafe].


Что касается вашего кода, вы можете безопасно использовать [unsafe] в приведенном выше сценарии. Хотя в документации есть предупреждение об использовании атрибута [unsafe], на самом деле это предупреждение не объясняет, почему не следует использовать [unsafe].

Короче говоря, вы можете использовать атрибут [unsafe], когда время жизни экземпляра объекта, на который ссылается [unsafe] ссылка, больше, чем время жизни самой ссылки.

Другими словами, вы должны убедиться, что у вас нет доступа к [unsafe] ссылке после того, как экземпляр объекта, на который он указывает, был освобожден, и все.

[unsafe] ссылки не обнуляются при уничтожении объекта, на который они указывают, и использование такой ссылки после исчезновения объекта приведет к исключениям нарушения прав доступа.

Замена атрибута [weak] на [unsafe] - это все, что вам нужно сделать, чтобы иметь правильно работающий код в том виде, в котором вы его представили.

  TDependantChild = class(TAggregatedObject, IChild)
  private
    [unsafe] fChild: IChild;
person Dalija Prasnikar    schedule 04.11.2017
comment
Спасибо @Dalija за очень четкий ответ! Я провожу дни, пытаясь понять, что происходит. Я также добавляю ссылку к другому соответствующему обсуждению, которое я нашел по предоставленной вами ссылке. В FastMM4 этот примерный проект генерирует ту же ошибку нарушения прав доступа, что и обсуждается там. - person VyPu; 05.11.2017