331 lines
9.3 KiB
ObjectPascal
331 lines
9.3 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
|
|
private
|
|
FNext: PItem;
|
|
FPrev: PItem;
|
|
function GetNext: PItem; inline;
|
|
public
|
|
|
|
Receiver: T; // The registered interface instance (the event sink).
|
|
property Next: PItem read GetNext;
|
|
property Prev: PItem read FPrev;
|
|
end;
|
|
|
|
// Notify function. If this results false, it will be removed from the notify list
|
|
TNotifyProc = reference to function(const Obj: T): Boolean;
|
|
|
|
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;
|
|
class var
|
|
FReverseOnNotify: Boolean;
|
|
class constructor CreateClass;
|
|
|
|
// 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;
|
|
|
|
private
|
|
function GetFirst: PItem;
|
|
|
|
public
|
|
// Initializes the list in an unlocked state.
|
|
class operator Initialize(out Dest: TMycNotifyList<T>);
|
|
// Safely clears all registered receivers and cleans up resources.
|
|
procedure Finalize;
|
|
// 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(const Func: TNotifyProc);
|
|
property First: PItem read GetFirst;
|
|
|
|
// If this is true, the list is reversed after each Notify, hoping for a better distrubution of events.
|
|
class property ReverseOnNotify: Boolean read FReverseOnNotify write FReverseOnNotify;
|
|
end;
|
|
|
|
implementation
|
|
|
|
class constructor TMycNotifyList<T>.CreateClass;
|
|
begin
|
|
FReverseOnNotify := true;
|
|
end;
|
|
|
|
class operator TMycNotifyList<T>.Initialize(out Dest: TMycNotifyList<T>);
|
|
begin
|
|
NativeUInt(Dest.FList) := 1;
|
|
end;
|
|
|
|
procedure TMycNotifyList<T>.Finalize;
|
|
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.FPrev := nil;
|
|
Item.FNext := FList;
|
|
if Item.FNext <> nil then
|
|
Item.FNext.FPrev := 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>.GetFirst: PItem;
|
|
begin
|
|
Assert(IsLocked);
|
|
Result := FList;
|
|
end;
|
|
|
|
function TMycNotifyList<T>.IsLocked: Boolean;
|
|
begin
|
|
Result := NativeUInt(FList) and 1 = 0;
|
|
end;
|
|
|
|
procedure TMycNotifyList<T>.Notify(const Func: TNotifyProc);
|
|
var
|
|
Item: PItem;
|
|
begin
|
|
Assert(IsLocked);
|
|
|
|
// Invariant: Append all detached items at end of the list. We can't simply free them, because the subscription is owned by the client.
|
|
|
|
if FReverseOnNotify then
|
|
begin
|
|
// Method: stack assigned items and rebuild the list from the stack (reversing the order)
|
|
|
|
var stack: PItem := nil;
|
|
var released: PItem := nil;
|
|
|
|
while (FList <> nil) and Assigned(FList.Receiver) do
|
|
begin
|
|
Item := FList;
|
|
FList := Item.FNext;
|
|
|
|
if Func(Item.Receiver) then
|
|
begin
|
|
// item stays valid
|
|
Item.FNext := stack;
|
|
stack := Item;
|
|
end
|
|
else
|
|
begin
|
|
// release the receiver
|
|
Item.Receiver := nil;
|
|
|
|
Item.FNext := released;
|
|
released := Item;
|
|
end;
|
|
end;
|
|
|
|
// The list now only contains old released items. Push the newly released ones.
|
|
while released <> nil do
|
|
begin
|
|
Item := released;
|
|
released := Item.FNext;
|
|
|
|
Item.FPrev := nil;
|
|
Item.FNext := FList;
|
|
FList := Item;
|
|
if Item.FNext <> nil then
|
|
Item.FNext.FPrev := Item;
|
|
end;
|
|
|
|
// Now push the valid items, so that they are at the beginning of the list.
|
|
while stack <> nil do
|
|
begin
|
|
Item := stack;
|
|
stack := Item.FNext;
|
|
|
|
Item.FPrev := nil;
|
|
Item.FNext := FList;
|
|
FList := Item;
|
|
if Item.FNext <> nil then
|
|
Item.FNext.FPrev := Item;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
// Method: filter released items and add then to the end of the list
|
|
|
|
var released: PItem := nil;
|
|
var lastValid: PItem := nil;
|
|
|
|
Item := FList;
|
|
while (Item <> nil) and Assigned(Item.Receiver) do
|
|
begin
|
|
if not Func(Item.Receiver) then
|
|
begin
|
|
// item is now invalid
|
|
if Item = FList then
|
|
FList := Item.FNext;
|
|
|
|
if Item.FPrev <> nil then
|
|
Item.FPrev.FNext := Item.FNext;
|
|
if Item.FNext <> nil then
|
|
Item.FNext.FPrev := Item.FPrev;
|
|
|
|
// release the receiver
|
|
Item.Receiver := nil;
|
|
|
|
// and save the list item in a tmp list for later use
|
|
var nxt := Item.FNext;
|
|
Item.FNext := released;
|
|
released := Item;
|
|
Item := nxt;
|
|
end
|
|
else
|
|
begin
|
|
// save a pointer to the last valid item
|
|
lastValid := Item;
|
|
Item := lastValid.FNext;
|
|
end;
|
|
end;
|
|
|
|
// insert all new released items after the last valid item
|
|
while released <> nil do
|
|
begin
|
|
Item := released;
|
|
released := Item.FNext;
|
|
|
|
if lastValid = nil then
|
|
begin
|
|
Item.FPrev := nil;
|
|
Item.FNext := FList;
|
|
FList := Item;
|
|
end
|
|
else
|
|
begin
|
|
Item.FPrev := lastValid;
|
|
Item.FNext := lastValid.Next;
|
|
if Item.FPrev <> nil then
|
|
Item.FPrev.FNext := Item;
|
|
end;
|
|
if Item.FNext <> nil then
|
|
Item.FNext.FPrev := Item;
|
|
end;
|
|
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);
|
|
Item.Receiver := nil;
|
|
|
|
if Item = FList then
|
|
FList := Item.FNext;
|
|
|
|
if Item.FPrev <> nil then
|
|
Item.FPrev.FNext := Item.FNext;
|
|
if Item.FNext <> nil then
|
|
Item.FNext.FPrev := Item.FPrev;
|
|
|
|
FreeItem(Item);
|
|
end;
|
|
|
|
procedure TMycNotifyList<T>.UnadviseAll;
|
|
begin
|
|
Assert(IsLocked);
|
|
while FList <> nil do
|
|
Unadvise(TTag(FList));
|
|
end;
|
|
|
|
function TMycNotifyList<T>.TItem.GetNext: PItem;
|
|
begin
|
|
if not Assigned(Receiver) then
|
|
exit(nil);
|
|
Result := FNext;
|
|
end;
|
|
|
|
end.
|