2

I find it hard to understand the reason why implementations of TXPSubject.DeleteObserver and TXPSubject.DeleteObservers in XPObserver.pas are different? Specifically, the different order of "Dispose" and "ReleaseSubject" calls, and the reasoning why the different order matters for TXPSubject.DeleteObserver. The code in question is extracted and shown below. The file is available from DUnit.

function TXPSubject.DeleteObserver(const Observer: IXPObserver;
  const Context: pointer): boolean;
var
  idx: integer;
  ObserverInfo: TXPObserverInfo;

begin
  FSync.Enter;

  try
    // Check for existence or prior removal
    Result := FindObserver(Observer, Context, idx);

    if Result then
    begin
      // Need local ref after deletion from list. Order of Delete() &
      // ReleaseSubject() is important here for correct functioning of _Release
      // ...***DON'T*** refactor this method!!
      ObserverInfo := PXPObserverInfo(FObservers[idx])^;
      // Release our (list) reference to observer
      PXPObserverInfo(FObservers[idx])^.Observer := nil;
      System.Dispose(FObservers[idx]);
      FObservers.Delete(idx);
    end;

    // Exit critical section here as we now have local vars only (thread-safe)
    // and call to ReleaseSubject below on last reference will leave FSync
    // invalid (destroyed).
  finally
    FSync.Leave;
  end;

  // Notify Observer to release reference to us. This will result in
  // a call to TXPSubject._Release.
  if Result then
    ObserverInfo.Observer.ReleaseSubject(IXPSubject(ObserverInfo.Subject),
      ObserverInfo.Context);

end;


procedure TXPSubject.DeleteObservers;
var
  idx: integer;
  ObserverInfo: PXPObserverInfo;

begin
  FDeletingObservers := true;
  // Count *down* to allow for side-effect of loop actions -
  // referenced item will be deleted from list, and remainder will move down
  // one slot.
  for idx := FObservers.Count - 1 downto 0 do
  begin
    ObserverInfo := FObservers[idx];
    // Notify Observer to release reference to Subject
    ObserverInfo^.Observer.ReleaseSubject(IXPSubject(ObserverInfo.Subject),
      ObserverInfo^.Context);
    // Release our (list) reference to Observer
    ObserverInfo^.Observer := nil;
    System.Dispose(ObserverInfo);
    FObservers.Delete(idx);
  end;

  FDeletingObservers := false;
end;
SOUser
  • 3,802
  • 5
  • 33
  • 63
  • 1
    The first one seems to be thread-safe and that's also why the ordering is different (`ReleaseSubject` is called outside the lock). The second one seems not to be thread-safe and honestly I don't know why. – jpfollenius Nov 13 '13 at 07:15
  • @jpfollenius Thank you for the comment very much! However currently I am confused more about the order of Dispose & ReleaseSubject than about the thread-safe treatment of the FObservers List. – SOUser Nov 13 '13 at 07:27
  • 1
    Then look at my answer, which explains why the ordering **must** be different in the thread-safe version. – jpfollenius Nov 13 '13 at 07:28
  • 1
    The code looks a little lame. I find the `Observer := nil` disappointing. Better to remove that and let `Dispose` do it. Albeit one of the two calls to `Dispose` would need a typecast. – David Heffernan Nov 13 '13 at 15:52
  • @DavidHeffernan Thank you for your comments ! Sorry for the confusion but it looks to me that (1) The `nil` statement will induce the `actual observer`'s `_release`. The `Dispose` statement only deals with the TXPObserverInfo record. That is to say, there is another layer. (2) The two calls to Dispose in DeleteObserver and DeleteObservers are semantically the same (?). – SOUser Nov 13 '13 at 15:56
  • 1
    @XichenLi No. Dispose acts recursively on the object that it is passed. It finalizes the contents of that object, and the contents of any objects that it contains an so on. And I'm using object in a very general way here. So call dispose and pass a typed pointer, and strings, interfaces etc. are tidied up. Now the two calls to Dispose that are under the microscope here differ semantically. One of them passes a typed pointer, the other passes untyped pointer of type `Pointer`. That's the one from `TList`. TBC. – David Heffernan Nov 13 '13 at 16:00
  • 1
    Only the variant with the typed pointer can perform tidy up. When passed an untyped pointer, `Dispose` is nothing more than `FreeMem`. So I would remove both lines that set `Observer := nil`. The call to `Dispose` in `TXPSubject.DeleteObservers` already is sufficient since it is passed a typed pointer. And `TXPSubject.DeleteObserver` needs to be modified like so: `Dispose(PXPObserverInfo(FObservers[idx]))`. – David Heffernan Nov 13 '13 at 16:02
  • Could you comment on the different orders of calling `Dispose` and `ReleaseSubject` in DeleteObserver and DeleteObservers ? – SOUser Nov 13 '13 at 16:08
  • It's exactly as @jpfollenius said. In `TXPSubject.DeleteObserver` it is essential that `FSync` is not referred to after the call to `ReleaseSubject`. Everything flows from that. – David Heffernan Nov 13 '13 at 21:28

2 Answers2

2

The second method is the more obvious implementation, which does not bother with thread-safety. The first one is the tricky one.

That first method uses a critical section FSync for thread-safety. Since TXPSubject is a TInterfacedObject (which cannot be seen in the provided code), it will be destroyed when the last reference to that object is dropped. That means that when the last observer is deleted using DeleteObserver which calls ReleaseSubject on that last observer (which should set the reference to the subject to nil), the object will be destroyed. Now if the ReleaseSubject would have been called within the lock, then the line

FSync.Leave;

causes an access violation because the object (and therefore also FSync) has been destroyed already.

So instead of calling ReleaseSubject from within the lock, a local copy of the observer info is created with

ObserverInfo := PXPObserverInfo(FObservers[idx])^;

This dereferences the pointer stored in the observer list and basically creates a local copy. Then, after the FSync lock has been released, ReleaseSubject is called on that local copy. After that point Self is not referenced any more so it does not matter that the object has been destroyed.

EDIT: Whoever calls DeleteObserver still holds a valid reference to ISubject so it should not get destroyed until this reference falls out of scope. So the scenario described in this answer only happens if the observer itself calls DeleteObserver (probably in its destructor) on the reference that is later reelased by ReleaseSubject.

jpfollenius
  • 16,456
  • 10
  • 90
  • 156
  • For record, what the observers hold is of record type TXPSubjectInfo. That is to say, the observers hold a raw pointer rather than a typed reference of IXPSubject. Therefore, the observers' `ReleaseSubject` should not trigger the subject's destruction (?). – SOUser Nov 13 '13 at 07:32
  • Thank you for your time and comments, but if your argument is valid, I would think that the same logic (about life-time treatment) should apply for TXPSubject.DeleteObservers (?). – SOUser Nov 13 '13 at 07:38
  • 1
    The difference is that there is no call to `FSync.Leave` after the last observer deleted. The call `FObservers.Delete(idx)` could be problematic as well as far as I understand it. David, could you help us out here? :) – jpfollenius Nov 13 '13 at 07:42
  • @DavidHeffernan Do you mean that the delicacy that the author put in TXPSubject.DeleteObserver is completely unnessary ? Or do you mean that the DeleteObservers should be refactored towards DeleteObserver ? – SOUser Nov 13 '13 at 16:00
  • 1
    I don't think the answer says that. – David Heffernan Nov 13 '13 at 16:02
2

1) DeleteObserver and DeleteObservers are used in different contexts, client and server, respectively. With the benefit of hindsight, I would now reduce the IXPSubject interface to simply a client interface to the subject

The client interface, used by Observers to register and de-register interest in a Subject:

  IXPSubject = interface(IXPObserver)
    ['{D7E3FD5D-0A70-4095-AF41-433E7E4A9C29}']
    function AddObserver(const Observer: IXPObserver;
      const Subject: IXPSubject; const Context: pointer = nil): boolean;
    function InsertObserver(const idx: integer; const Observer: IXPObserver;
      const Subject: IXPSubject; const Context: pointer = nil): boolean;
    function DeleteObserver(const Observer: IXPObserver;
      const Context: pointer = nil): boolean;

    function ObserverCount: integer;
    function GetObserver(const idx: integer): IXPObserver;
    property Observers[const idx: integer]: IXPObserver read GetObserver;
    property Count: integer read ObserverCount;
  end;

DeleteObservers is only called indirectly, by TXPSubject._Release. This method does not use the critical section, FSync, because it's use within _Release is already guarded by FSync. DeleteObservers should really be a private method on the class implementing IXPSubject, TXPSubject

You'll notice that DeleteObservers is called conditionally in TXPSubject._Release. This is when the primary reference to the Subject, created before any Observers are registered, is being released. Typically, this will be when the container object for the Subject is being destroyed.

2) Ordering of methods

In TXPSubject.DeleteObserver:

  // Release our (list) reference to observer
  PXPObserverInfo(FObservers[idx])^.Observer := nil;
  System.Dispose(FObservers[idx]);
  FObservers.Delete(idx);
  ObserverInfo.Observer.ReleaseSubject(IXPSubject(ObserverInfo.Subject),
    ObserverInfo.Context);

In TXPSubject.DeleteObservers:

  // Notify Observer to release reference to Subject
  ObserverInfo^.Observer.ReleaseSubject(IXPSubject(ObserverInfo.Subject),
    ObserverInfo^.Context);
  // Release our (list) reference to Observer
  ObserverInfo^.Observer := nil;
  System.Dispose(ObserverInfo);

Observer-initiated de-registration

As mentioned earlier, Subject.DeleteObserver is called by an Observer when it wishes to to de-register itself from Subject - when the Observer initiates the de-registration process. The effect of this is for the Subject to finalise its internal state (removing references to Observer), before it calls (temp thread-local copy of) Observer.ReleaseSubject

In Observer.ReleaseSubject, the Observer's reference to Subject is nil'd, indirectly causing a call to Subject._Release...

TXPSubject._Release checks to see if the primary reference to itself is being released. If we had not finalised the internal state of the Subject before calling Observer.ReleaseSubject, we would now, erroneously, call DeleteObservers. The correct behaviour in this situation requires that ReleaseSubject is called last.

Subject-initiated de-registration

The Subject initiates de-registration when the primary reference is being released. As mentioned earlier, typically, this will be when the container object for the Subject is being destroyed, or a singleton (possibly unit-scoped or global) reference is destroyed on application shutdown.

We can see in TXPSubject._Release, that DeleteObservers will be called in this situation. The flag FDeletingObservers is set on entry to DeleteObservers to prevent re-entrancy when Observers releasing their references to Subject indirectly call TXPSubject._Release.

Subject must have a reference to Observer to be able to call Observer.ReleaseSubject, so the finalising of internal state occurs after the call to Observer.ReleaseSubject in DeleteObservers.

I hope this helps to clarify the situation for you Li?

Cheers, Paul.

pvspain
  • 41
  • 2
  • Why did you explicitly write `Observer := nil` rather then letting Dispose do it? – David Heffernan Nov 15 '13 at 07:01
  • I don't know - ignorance perhaps? I wrote this about a decade and several languages ago. I'm surprised and chuffed that anyone would still care. – pvspain Nov 15 '13 at 07:10
  • I'm amazed that you still remember the details. When I implemented this pattern recently it heavily used generics which helped. I'm sure your code could be simpler, but it is doing quite a lot more than a plain vanilla observer. Thanks for your answer. – David Heffernan Nov 15 '13 at 07:24