unit Myc.Core.Notifier; interface uses System.SysUtils, System.SyncObjs; type // Low-level implementation for thread-safe multicast events. // Implemented as a list referencing IInterface instances, with a locking mechanism for thread safety. // Utilizes minimal memory. // `Advise` adds an interface and returns a tag for constant-time removal by `Unadvise`. // `UnadviseAll` removes all registered interfaces. // After `Finalize` is called, the list is cleared, and no new interfaces can be added (closed state). TMycNotifyList = record type TTag = Pointer; // Opaque tag used to identify a registered receiver for unsubscription. PItem = ^TItem; // Pointer to an internal list item. TItem = record // Internal structure for storing a receiver and list linkage. Next, Prev: PItem; // Pointers to the next and previous items in the doubly linked list. Receiver: T; // The registered interface instance (the event sink). end; strict private // Bit 0: Lock state (0 = locked, 1 = unlocked). // Bit 1: Finalized state (0 = not finalized, 1 = finalized). // Other bits (if not 0 and bit 0 is 1) can be a direct interface pointer if FList is nil. FList: PItem; // Head of the linked list for additional receivers beyond the first one. class function AllocItem: PItem; static; inline; // Allocates and initializes memory for a new TItem. class procedure FreeItem(Item: PItem); static; inline; // Frees memory previously allocated for a TItem. public procedure Create; // Initializes the notification list, preparing it for use. procedure Destroy; // Cleans up all resources, including unadvising all receivers. Assumes no concurrent access. function Advise(const Receiver: T): TTag; // Registers a receiver interface and returns an opaque tag for later unsubscription. procedure Unadvise(Tag: TTag); procedure UnadviseAll; // Unregisters all currently advised receivers. procedure Lock; inline; // Acquires an exclusive lock for thread-safe operations on the list. procedure Release; inline; // Releases the previously acquired exclusive lock. function IsLocked: Boolean; inline; // Checks if the list is currently locked by any thread. procedure Notify( Func: TPredicate ); // Iterates through registered receivers and invokes the predicate; removes receiver if predicate returns false. end; implementation procedure TMycNotifyList.Create; begin NativeUInt(FList) := 1; end; procedure TMycNotifyList.Destroy; begin // Because refcounting is thread-safe, this will always be entered once after all references // to Self are dropped. No locking needed! // Assert(not IsLocked); NativeUInt(FList) := NativeUInt(FList) and not 1; UnadviseAll; end; function TMycNotifyList.Advise(const Receiver: T): TTag; var Item: PItem; begin Assert(IsLocked); if not Assigned(Receiver) then exit(nil); Item := AllocItem; Item.Receiver := Receiver; Item.Prev := nil; Item.Next := FList; if Item.Next <> nil then Item.Next.Prev := Item; FList := Item; exit(Item); end; procedure TMycNotifyList.Lock; begin while not TInterlocked.BitTestAndClear(NativeUint(FList), 0) do YieldProcessor; Assert(IsLocked, 'Locking failed'); end; class function TMycNotifyList.AllocItem: PItem; begin Result := AllocMem(sizeof(TItem)); end; class procedure TMycNotifyList.FreeItem(Item: PItem); begin FreeMem(Item, sizeof(TItem)); end; function TMycNotifyList.IsLocked: Boolean; begin Result := NativeUInt(FList) and 1 = 0; end; procedure TMycNotifyList.Notify(Func: TPredicate); var Item: PItem; Last: PItem; tmp: PItem; begin Assert(IsLocked); tmp := nil; Last := nil; Item := FList; while (Item <> nil) and Assigned(Item.Receiver) do begin if not Func(Item.Receiver) then begin // Receiver wants no more notifications, detach it if Item = FList then FList := Item.Next; if Item.Prev <> nil then Item.Prev.Next := Item.Next; if Item.Next <> nil then Item.Next.Prev := Item.Prev; // release the receiver Item.Receiver := nil; // and save the list item in a tmp list for later use var nxt := Item.Next; Item.Next := tmp; tmp := Item; Item := nxt; end else begin Last := Item; Item := Last.Next; end; end; // Append all detached items at end of the list. We can't simply free them, because the subscription is owned by the client. while tmp <> nil do begin Item := tmp; tmp := Item.Next; if Last = nil then begin Item.Prev := nil; Item.Next := FList; FList := Item; end else begin Item.Prev := Last; Item.Next := Last.Next; if Item.Prev <> nil then Item.Prev.Next := Item; end; if Item.Next <> nil then Item.Next.Prev := Item; end; end; procedure TMycNotifyList.Release; begin Assert(IsLocked); TInterlocked.Exchange(NativeUint(FList), NativeUint(FList) or 1); end; procedure TMycNotifyList.Unadvise(Tag: TTag); var Item: PItem; begin Assert(IsLocked); if (Tag = nil) or (FList = nil) then exit; {$ifdef DEBUG} Item := FList; while Item <> nil do begin if Item = Tag then break; Item := Item.Next; end; Assert(Item <> nil, 'Receiver not advised'); {$endif} if FList = nil then exit; Item := PItem(Tag); if Item = FList then FList := Item.Next; if Item.Prev <> nil then Item.Prev.Next := Item.Next; if Item.Next <> nil then Item.Next.Prev := Item.Prev; Item.Receiver := nil; FreeItem(Item); end; procedure TMycNotifyList.UnadviseAll; begin Assert(IsLocked); while FList <> nil do Unadvise(TTag(FList)); end; end.