Pull to refresh

Comments 26

В старых не хранят Pointer, а используют приведение к нему при сохранении интерфейса.
Можно поподробнее? Что значит «при сохранении интерфейса»?
Плохо выразился. В старых не надо объявлять поле типа Pointer, в старых объявляется поле интерфейсного типа, а при присвоении ему значения делается приведение: Pointer(FInterfaceField) := Pointer(InterfaceVariable);
Нигде не встречал такой конструкции. А зачем так делать? Чтобы получить AV при выходе поля за область видимости? Или вы предлагаете вручную чистить поля потом?
Так делали, чтобы получить weak-ссылку и при этом не заниматься многочисленными приведением Pointer к интерфейсу.
Не будет никакого AV, если вы руками присвоите nil в деструкторе, тоже через приведение к Pointer.
Не будет AV если… Это в любом случае отчаянная попытка выстрелить себе в ногу. Через пол года откроете модуль, увидите там FInterfacedFiled: IMyInterface, и забудете вот так Pointer(FInterfaceField) := Pointer(InterfaceVariable) перезаписать. Да и на практике я подобные вещи ни разу в жизни не встречал.
А чтобы избежать постоянного тайпкаста обычно делается следующее:
TMyClass = class
private
  FInterfaceField: Pointer;
  function GetInterfaceField: IMyInterface;
  procedure SetInterfaceField(const Value: IMyInterface);
  property InterfaceField: IMyInterface read GetInterfaceField write SetInterfaceField;
end;

TMyClass.GetInterfaceField: IMyInterface;
begin
  Result := IMyInterface(FInterfaceField);
end;

TMyClass.SetInterfaceField(const Value: IMyInterface);
begin
  FInterfaceField := Pointer(Value);
end;
И пользуются после этого свойством.
Скомпилировалось в Delphi 2010 — основной на работе.
За статью спасибо, однозначно в избранное.
А вам точно нужны эти интерфейсы везде? Мне кажется в большинстве приведенных примеров можно было обойтись просто переменными-классами нужного уровня абстракции с ручным разрушением подчиненных объектов в деструкторах родителей. Или все это с целью не отслеживать вручную время жизни объектов? Я согласен, лень двигатель прогресса, но все же все эти наслоения выглядят иногда слишком.

Опять же такой вопрос, вот выяснили вы что WeakRef.IsAlive = false; Что программа должна с этим делать? Создать новый объект или выкинуть исключение? Или проигнорировать? Я допускаю что где-то объективно применимо одно из этих трех действий. Но в подавляющем большинстве — это ненормальное поведение, не так ли?

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

Мы создали в одном месте некоторый объект А, передали указатель на него какому-то другому объекту Б. Допустим так случилось что объект А умирает раньше объекта Б. В этом случае нам надо как-то почистить указатель в объекте Б. Нужно придумывать механизм чистки ссылок. Хороший пример — TComponent со своим FreeNotification. Там создаются списки, компоненты друг другу добавляются в списки, при уничтожении одного из них они друг у друга удаляются из списков. При этом надо не забыть перекрыть Notification, чтобы подчистить поля.
Ну ужас же. Разве нет? Ужас с точки зрения производительности, и ужас с точки зрения отладки.

А IsAlive — это тоже самое что Assigned, но только для этих weak ссылок. Программа должна вести себя ровно так же, как если бы там был просто указатель на TObject равный nil. То есть эти weak ссылки надо использовать там, где подразумевается такое состояние, а это очень много где. Это и механизм подписчиков. И когда плагины друг на друга ссылаются, или скажем контроллеры в MVC. Да те же TComponent можно было реализовать без всяких FreeNotification.
Не, я согласен, механизм вы придумали хороший, правильный. Тем более если его применение подтверждается практикой. Как минимум время на разработку он сэкономит и простит многие вольности в управлении памятью.

В подписках я обычно делал примерно так: подписчик получает указатель на список в котором он подписан. В деструкторе подписчик соответственно удаляет себя из этого списка. Не панацея конечно, но в большинстве случаев хватало
То есть вместо заботы о подчистке создаваемых объектов получаем заботу о силе ссылки?
Не так. Вместо заботы о подчистке всюду ссылок на уничтожаемый объект получаем заботу о силе ссылки. Вместо того чтобы думать как нам подчистить потом эту ссылку — мы просто используем слабую ссылку и все.
Предлагаю обсудить эту тему с разработчиками FPC, это может стать частью базовой библиотеки.
Да, думаю стоит попробовать, спасибо.
Для простых уведомлений хорошо подходят message methods — они проще и надежнее интерфейсов.
Они настолько разные по возможностям, и результату — что я не берусь сравнивать их. В одних случаях message удобнее, в других лучше нотификации через подписчиков. В любом случае тема не совсем об этом.
Довольно опасная штука.
Обычно объект отписывается отовсюду, а потом начинает разрушаться.
Здесь же сначала выполняется деструктор, а по его завершении происходит отписка.

В многопоточной среде легко словить ситуацию, когда событие приходит разрушенному объекту.
Не могу с вами согласиться. В многопоточном коде возможны 2 варианта:
1. Нужно учитывать, что после Unsubscribe может прийти эвент всегда.
2. Unsubscribe вызывает блокировку одной критической секции с кодом, который файрит эвенты. Сама по себе такая блокировка опасна, ибо получить дедлок при таком подходе элементарно.
Я использую только 1-ый подход. В обработчиках события всегда учитываю разрушается объект нет.
Первый подход накладывает дополнительные ограничения на пользовательские классы, в обработчиках надо везде вставлять
if (FIsDestroying) then exit;

Я считаю, это просто некрасиво.

Второй подход в том виде, как выше описан, действительно легко приводит к дедлокам, но я использую следующий трюк:
Перед циклом, который файрит эвенты, копирую список вызываемых функций в массив, и только потом вызываю делегаты из массива. У каждого паблишера своя критическая секция.
Перед циклом, который файрит эвенты, копирую список вызываемых функций в массив, и только потом вызываю делегаты из массива. У каждого паблишера своя критическая секция.

В том то и дело что именно так — не корректно. После копирования в массив критическая секция уже отпущена. Другой поток может вызвать Unsubscribe в тот момент, когда уже скопированы но еще не сфайрены. Таким образом евент прилетит в уже уничтожающийся (отписавшийся) объект, и все равно надо делать что-то в духе if (FIsDestroying) then exit; чтобы все было корректно.

p.s. Кстати если вы обратите внимание, то TBasePublisher.GetItems как раз возвращает массив. В архиве в примере к статье есть вот такая реализация:
function TBasePublisher.GetItems: TWeakRefArr;
var i: Integer;
begin
  if assigned(FCS) then
  begin
    FCS.Enter;
    try
      SetLength(Result, FCount);
      for i := 0 to FCount - 1 do
        Result[i] := FItems[i];
    finally
      FCS.Leave;
    end;
  end
  else
    Result := FItems;
end;
Критическая секция создается если класс создается как ThreadSafe в конструкторе:
constructor TBasePublisher.Create(const AThreadSafe: Boolean);
begin
  if AThreadSafe then FCS := TCriticalSection.Create;
end;
Ну то есть я делаю ровно так же, как вы только что описали ;)
Не-не-не, я не отпускаю критическую секцию при вызове делегатов.

Копирование нужно для такого сценария:
В момент передачи события объекту он отписывается от этого паблишера и поскольку в Unsubscribe список защищён той же секцией, что и в броадкаст-методе, а текущий поток уже владеет секцией, отписка модицирует список и итерировать по нему дальше нельзя.

Наверное можно придумать сценарий дедлока, но пока такая схема — у каждого паблишера своя секция и она не отпускается при вызове событий — себя неплохо показывает.
Не-не-не, я не отпускаю критическую секцию при вызове делегатов.
Сценарий дедлока возникает легко. Допустим у нас есть VCL и не основной поток, который файрит нотификации. Итак он сфайрил нотификацию, и вызов попадает в обработчик:
procedure TSubscriber.OnBlaBla(Sender: IUnknown)
begin
  //что то делаем
  //и вызваем синхронизацию в основной поток для работы с VCL
  Synchronize(DoSomething); 
  //если кто-то в процессе работы DoSomething вызовет Subscribe/Unsubscribe,
  //или создаст ситуацию когда сфайрится евент из основного потока - это будет гарантированный дедлок
end;
Увы, я слишком много стрелял себе в ногу дедлоками и сразу вижу потенциально опасные места. Единственно безопасный вариант — это не держать критическую секцию у паблишера заблокированной, когда файрятся евенты.
Но здесь хоть можно подумать цикл зависимостей блокировок и доказать корректность кода, а что делать с конструкцией
if (FIsDestroying) then exit;

Всё, начиная от этого условия до конца деструктора (до момента, когда будет обнулён указатель в IWeakly), нужно оборачивать с критическую секцию? И получение указателя из IWeakly — тоже в этой секции? Иначе возникнет ситуация, когда нотификация начала обрабатываться, и деструктор уже прошёл этот if

Из тех же соображений конструкция
if (weakptr.IsAlive) then ptr := weakptr.Get;

должна быть атомарной (в одном методе, защищённом секцией).
В многопоточном коде сначала получаем указатель, потом проверяем:
ptr := weakptr.Get;
if Assigned(ptr) then
Да, оборачивать критической секцией. Вместе с условием.
FDestroyCS.Enter;
try
  if (FIsDestroying) then exit;
  //process notification
finally
  FDestroyCS.Leave;
end;

И получение указателя из IWeakly — тоже в этой секции?
Нет, зачем же? Получение указателя из IWeakly выполняется паблишером. Ниже написал как многопоточный случай должен обрабатываться.

Но в случае с таким подходом мы гарантированно избегаем дедлока, а валидный дестрой объекта обеспечивается только в контексте самого объекта. Мы точно знаем что и как нам нужно сделать (чуть ли не шаблонные действия).
А вот если мы захотим обезопасить себя от дедлока — это практически нереально. Нужно постоянно помнить про то что у нас паблишер залочен. Любой рефакторинг через пол года/год — и привет дедлок. Хотя я раньше тоже был сторонником такого подхода.
Sign up to leave a comment.

Articles