233 lines
6.2 KiB
ObjectPascal
233 lines
6.2 KiB
ObjectPascal
unit Myc.Core.Notifier;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils,
|
|
System.SyncObjs;
|
|
|
|
type
|
|
// A thread-safe, low-level list for managing multicast event sinks of type IInterface.
|
|
// It uses a doubly linked list and a spinlock for synchronization.
|
|
// `Advise` registers a sink and returns a tag for constant-time removal via `Unadvise`.
|
|
TMycNotifyList<T: IInterface> = record
|
|
type
|
|
// Opaque tag used to identify a registered receiver for unsubscription.
|
|
TTag = Pointer;
|
|
|
|
// Pointer to an internal list item.
|
|
PItem = ^TItem;
|
|
// Internal structure for storing a receiver and list linkage.
|
|
TItem = record
|
|
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
|
|
// Head of the linked list. The pointer value itself is repurposed for a spinlock.
|
|
// Bit 0 of the address stores the lock state (0 = locked, 1 = unlocked).
|
|
[volatile]
|
|
FList: PItem;
|
|
|
|
// Allocates memory for a new TItem.
|
|
class function AllocItem: PItem; static; inline;
|
|
// Frees memory previously allocated for a TItem.
|
|
class procedure FreeItem(Item: PItem); static; inline;
|
|
|
|
public
|
|
// Initializes the list in an unlocked state.
|
|
procedure Create;
|
|
// Safely clears all registered receivers and cleans up resources.
|
|
procedure Destroy;
|
|
// Registers a receiver interface and returns an opaque tag for later unsubscription.
|
|
function Advise(const Receiver: T): TTag;
|
|
// Unregisters a single receiver using its subscription tag.
|
|
procedure Unadvise(Tag: TTag);
|
|
// Unregisters all currently advised receivers.
|
|
procedure UnadviseAll;
|
|
// Acquires an exclusive lock for thread-safe operations on the list.
|
|
procedure Lock; inline;
|
|
// Releases the previously acquired exclusive lock.
|
|
procedure Release; inline;
|
|
// Checks if the list is currently locked by any thread.
|
|
function IsLocked: Boolean; inline;
|
|
// Invokes a predicate for each registered receiver.
|
|
// If the predicate returns false, the receiver is detached from further notifications
|
|
// by setting its interface reference to nil. The list item itself is not freed here.
|
|
procedure Notify(Func: TPredicate<T>);
|
|
end;
|
|
|
|
implementation
|
|
|
|
procedure TMycNotifyList<T>.Create;
|
|
begin
|
|
NativeUInt(FList) := 1;
|
|
end;
|
|
|
|
procedure TMycNotifyList<T>.Destroy;
|
|
begin
|
|
Lock;
|
|
UnadviseAll;
|
|
end;
|
|
|
|
function TMycNotifyList<T>.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<T>.Lock;
|
|
begin
|
|
while not TInterlocked.BitTestAndClear(NativeUint(FList), 0) do
|
|
YieldProcessor;
|
|
|
|
Assert(IsLocked, 'Locking failed');
|
|
end;
|
|
|
|
class function TMycNotifyList<T>.AllocItem: PItem;
|
|
begin
|
|
Result := AllocMem(sizeof(TItem));
|
|
end;
|
|
|
|
class procedure TMycNotifyList<T>.FreeItem(Item: PItem);
|
|
begin
|
|
FreeMem(Item, sizeof(TItem));
|
|
end;
|
|
|
|
function TMycNotifyList<T>.IsLocked: Boolean;
|
|
begin
|
|
Result := NativeUInt(FList) and 1 = 0;
|
|
end;
|
|
|
|
procedure TMycNotifyList<T>.Notify(Func: TPredicate<T>);
|
|
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<T>.Release;
|
|
begin
|
|
Assert(IsLocked);
|
|
TInterlocked.BitTestAndSet(NativeUint(FList), 0);
|
|
end;
|
|
|
|
procedure TMycNotifyList<T>.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<T>.UnadviseAll;
|
|
begin
|
|
Assert(IsLocked);
|
|
while FList <> nil do
|
|
Unadvise(TTag(FList));
|
|
end;
|
|
|
|
end.
|