diff --git a/Src/Myc.Core.Notifier.pas b/Src/Myc.Core.Notifier.pas new file mode 100644 index 0000000..74786e5 --- /dev/null +++ b/Src/Myc.Core.Notifier.pas @@ -0,0 +1,204 @@ +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 + FFirst: NativeUInt; // Stores the first receiver if no list is allocated, or acts as a combined lock and state field. + // 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); // Unregisters a specific receiver using the tag obtained from Advise. + procedure UnadviseAll; // Unregisters all currently advised receivers. + procedure Finalize; // Clears all receivers and permanently prevents new interfaces from being added. + 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. + function IsFinalized: Boolean; inline; // Checks if the list has been finalized and no longer accepts new receivers. + procedure Notify(Func: TPredicate); // Iterates through registered receivers and invokes the predicate; removes receiver if predicate returns false. + end; + +implementation + +procedure TMycNotifyList.Create; +begin + FFirst := 1; + FList := nil; +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 ); + FFirst := FFirst and not 3; + UnadviseAll; +end; + +function TMycNotifyList.Advise(const Receiver: T): TTag; +var + Item: PItem; +begin + Assert( IsLocked ); + + if IsFinalized then + exit( 0 ); + + if FFirst=0 then + begin + IInterface( FFirst ) := Receiver; + exit( PPointer(@Receiver)^ ); + end; + + 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 + repeat + if FFirst and 1 = 0 then + begin + YieldProcessor; + continue; + end; + until TInterlocked.BitTestAndClear( PNativeUint( @FFirst )^, 0 ); + + Assert( IsLocked, 'Locking failed' ); +end; + +class function TMycNotifyList.AllocItem: PItem; +begin + Result := AllocMem( sizeof( TItem ) ); +end; + +procedure TMycNotifyList.Finalize; +begin + Assert( IsLocked ); + UnadviseAll; + FFirst := FFirst or 2; +end; + +class procedure TMycNotifyList.FreeItem(Item: PItem); +begin + FreeMem( Item, sizeof( TItem ) ); +end; + +function TMycNotifyList.IsLocked: Boolean; +begin + Result := FFirst and 1 = 0; +end; + +function TMycNotifyList.IsFinalized: Boolean; +begin + Result := FFirst and 2 <> 0; +end; + +procedure TMycNotifyList.Notify(Func: TPredicate); +var + Item, P: PItem; +begin + Assert( IsLocked ); + + if IsFinalized then + exit; + + if FFirst<>0 then + if not Func( IInterface( FFirst ) ) then + IInterface( FFirst ) := nil; + + Item := FList; + while Item<>nil do + begin + P := Item.Next; + if not Func( Item.Receiver ) then + Unadvise( TTag( Item ) ); + Item := P; + end; +end; + +procedure TMycNotifyList.Release; +begin + Assert( IsLocked ); + TInterlocked.Exchange( Pointer( FFirst ), Pointer( FFirst or 1 ) ); +end; + +procedure TMycNotifyList.Unadvise(Tag: TTag); +var + Item: PItem; +begin + Assert( IsLocked ); + + if IsFinalized then + exit; + + if NativeUInt(Tag) = FFirst then + begin + IInterface( FFirst ) := nil; + exit; + end; + + 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 ); + + if IsFinalized then + exit; + + IInterface( FFirst ) := nil; + while FList<>nil do + Unadvise( TTag( FList ) ); +end; + +end. diff --git a/Src/Myc.Signals.pas b/Src/Myc.Signals.pas index f8c7a89..998be29 100644 --- a/Src/Myc.Signals.pas +++ b/Src/Myc.Signals.pas @@ -1,1009 +1,200 @@ -////////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// -/// -/// Mycelium Framework -/// ------------------ -/// -/// (c)2007-2019 Michael Schimmel -/// Ehrenhainstraße 40 -/// 42329 Wuppertal / Germany -/// info@mycelium.net -/// -/// All rights reserved. -/// -////////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - unit Myc.Signals; interface uses - System.SysUtils, System.SyncObjs, - Myc.Atomic; + System.SysUtils, Myc.Core.Notifier; -{.$define DEBUG_SIGNALS} type - IMyc2Sink = interface + IMycSemaphore = interface; + + TMycSignal = class; + + IMycSubscriber = interface function Notify: Boolean; end; - IMyc2Flag = interface( IMyc2Sink ) - function Reset: Boolean; + TMycSubscription = record + private + FSignal: TMycSignal; + FTag: TMycNotifyList.TTag; + public + procedure Setup(ASignal: TMycSignal; ATag: TMycNotifyList.TTag); inline; + class operator Assign(var Dest: TMycSubscription; const [ref] Src: + TMycSubscription); + class operator Initialize(out Dest: TMycSubscription); + class operator Finalize(var Dest: TMycSubscription); end; - {$message hint 'Replace TSignalTag by IMycDeletable? '} - TSignalTag = NativeInt; - - IMyc2Signal = interface - function Advise( const Sink: IMyc2Sink ): TSignalTag; - procedure Unadvise( Tag: TSignalTag ); + IMycSignal = interface + function Subscribe(const Subscriber: IMycSubscriber): TMycSubscription; end; - IMyc2Notifier = interface( IMyc2Sink ) - {$region 'property access'} - function GetSignal: IMyc2Signal; - {$endregion} - property Signal: IMyc2Signal read GetSignal; - end; - - IMyc2State = interface( IMyc2Signal ) - {$region 'property access'} - function GetIsSet: Boolean; - {$endregion} - property IsSet: Boolean read GetIsSet; - end; - - IMyc2Condition = interface( IMyc2Sink ) - {$region 'property access'} - function GetState: IMyc2State; - {$endregion} - property State: IMyc2State read GetState; - end; - - IMyc2Event = interface( IMyc2Condition ) - function Reset: Boolean; - end; - - TMycSinkList = record - type - PItem = ^TItem; - - TItem = record - Next, Prev: PItem; - Sink: IMyc2Sink; - end; - + TMycSignal = class abstract( TInterfacedObject, IMycSignal ) strict private - FFirst: NativeUInt; - FList: PItem; - - {$ifdef DEBUG_SIGNALS} - FCount: Integer; - {$endif} - class var - [volatile] FItems: TMycAtomicStack; - [volatile] - FPendingCnt: Int64; - [volatile] - FUsedCnt: Int64; - - class constructor CreateClass; - class destructor DestroyClass; - class function AllocItem: PItem; static; inline; - class procedure FreeItem( Item: PItem ); static; inline; - - public - procedure Create; - procedure Destroy; - function Advise( const Sink: IMyc2Sink ): TSignalTag; - procedure Unadvise( Tag: TSignalTag ); - procedure UnadviseAll; + FSubscribers: TMycNotifyList; + private + procedure Unsubscribe(Tag: TMycNotifyList.TTag); + protected + procedure Lock; + procedure Unlock; procedure Finalize; - procedure Lock; inline; - procedure Release; inline; - function IsLocked: Boolean; inline; - function IsFinalized: Boolean; inline; procedure Notify; - end; - - TMyc2Signal = class( TInterfacedObject, IMyc2Signal, IMyc2State, IMyc2Sink ) - private - FList: TMycSinkList; - - class var - FNull: IMyc2Condition; - - protected - class constructor CreateClass; - class destructor DestroyClass; - function GetIsSet: Boolean; virtual; - function DoNotify: Boolean; virtual; public - constructor Create; - destructor Destroy; override; - - function Advise( const Sink: IMyc2Sink ): TSignalTag; - procedure Unadvise( Tag: TSignalTag ); - function Notify: Boolean; - - class function CreateAdvised( Count: Integer; const Getter: TFunc ): IMyc2Signal; overload; - class function IsStatic( const Signal: IMyc2Signal ): Boolean; overload; inline; - class function IsStatic( const Sink: IMyc2Sink ): Boolean; overload; inline; - - class function MakeValid( const Signal: IMyc2Signal ): IMyc2Signal; overload; static; inline; - class function MakeValid( const Signals: array of IMyc2Signal ): TArray; overload; static; - - class function CreateCounter( Count: Integer ): IMyc2Condition; static; inline; - class function CreateEvent( Init: Boolean ): IMyc2Event; static; inline; - class function CreateGroup( Count: Integer; const Getter: TFunc ): IMyc2Signal; overload; - class function CreateNotifier: IMyc2Notifier; static; inline; - - class function CreateUnion( const Sigs: array of IMyc2Signal ): IMyc2Signal; static; - class function CreateConcat( const States: array of IMyc2State ): IMyc2State; static; - - property IsSet: Boolean read GetIsSet; - class property Null: IMyc2Condition read FNull; + function Subscribe(const Subscriber: IMycSubscriber): TMycSubscription; + property Subscribers: TMycNotifyList read FSubscribers; end; - TMyc2Notifier = class( TMyc2Signal, IMyc2Notifier ) - protected - function GetSignal: IMyc2Signal; - end; - - TMyc2Condition = class( TMyc2Signal, IMyc2Condition ) - protected - function GetState: IMyc2State; - end; - - TMyc2Event = class( TMyc2Condition, IMyc2Event ) - private - [volatile] - FValue: Integer; - - protected - function DoNotify: Boolean; override; - function GetIsSet: Boolean; override; - - public - constructor Create( AInit: Boolean ); - function Reset: Boolean; virtual; - property IsSet: Boolean read GetIsSet; - end; - - TMyc2CountSignal = class sealed( TMyc2Condition ) - private - [volatile] - FCount: Integer; - - protected - function GetIsSet: Boolean; override; - function DoNotify: Boolean; override; - - public - constructor Create( ACount: Integer ); - end; - - TMyc2SinkProc = class sealed( TInterfacedObject, IMyc2Sink ) - private - FOnce: Boolean; - FProc: TProc; - - function Notify: Boolean; - - public - constructor Create( AOnce: Boolean; const AProc: TProc ); - class function CreateSink( Once: Boolean; const Proc: TProc ): IMyc2Sink; static; - end; - - TMyc2NullState = class sealed( TInterfacedObject, IMyc2Signal, IMyc2State ) - protected - function Advise( const Sink: IMyc2Sink ): TSignalTag; + IMycState = interface( IMycSignal ) + {$region 'property access'} function GetIsSet: Boolean; - procedure Unadvise( Tag: TSignalTag ); - procedure UnadviseAll; + {$endregion} + property IsSet: Boolean read GetIsSet; end; - TMyc2NullSink = class sealed( TInterfacedObject, IMyc2Sink, IMyc2Condition ) - private - FState: IMyc2State; - function GetState: IMyc2State; + IMycSemaphore = interface(IMycSubscriber) + {$region 'property access'} + function GetState: IMycState; + {$endregion} + property State: IMycState read GetState; + end; + TMycSemaphore = class(TMycSignal, IMycSemaphore, IMycState) + private + [volatile] FCount: Integer; + function GetState: IMycState; + function GetIsSet: Boolean; protected function Notify: Boolean; - public - constructor Create; - destructor Destroy; override; + constructor Create(ACount: Integer); end; - TMyc2Flag = class( TInterfacedObject, IMyc2Sink, IMyc2Flag ) - private - FFlag: Integer; - function Notify: Boolean; - function Reset: Boolean; - - public - constructor Create( AFlag: Boolean ); - end; - - TMycObserver = class - type - TNull = class( TInterfacedObject, IMycObserver ) - private - FSink: T; - function GetItem: T; - - public - constructor Create( const ASink: T ); - end; - - TOne = class( TNull ) - private - FSignal: IMyc2Signal; - FTag: TSignalTag; - - public - constructor Create( const ASignal: IMyc2Signal; const ASink: T ); - destructor Destroy; override; - end; - - TArr = class( TNull ) - private - FSignals: TArray; - FTags: TArray; - - public - constructor Create( const ASignals: TArray; const ASink: T ); - destructor Destroy; override; - end; - - public - class function CreateObserver( const Signals: array of IMyc2Signal; const Item: T ): IMycObserver; static; - end; - - TMycSignalUnion = class( TInterfacedObject, IMyc2Signal ) - private - FNotifier: IMyc2Notifier; - FSignals: TArray; - FTags: TArray; - - function Advise( const Sink: IMyc2Sink ): TSignalTag; - procedure Unadvise( Tag: TSignalTag ); - - public - constructor Create( const ASignals: array of IMyc2Signal ); - destructor Destroy; override; + Signals = record + class function CreateSemaphore(Count: Integer): IMycSemaphore; static; end; implementation -{ TMyc2Signal } +uses + System.SyncObjs; -constructor TMyc2Signal.Create; +procedure TMycSignal.Finalize; begin - inherited Create; - FList.Create; + FSubscribers.Finalize; end; -destructor TMyc2Signal.Destroy; +procedure TMycSignal.Lock; begin - FList.Destroy; - inherited Destroy; + FSubscribers.Lock; end; -class function TMyc2Signal.CreateAdvised( Count: Integer; const Getter: TFunc ): IMyc2Signal; -var - i, j: Integer; - Sig: IMyc2Signal; - Res: IMyc2Condition; +procedure TMycSignal.Notify; begin - for i := 0 to Count - 1 do - begin - Sig := Getter( i ); - if not IsStatic( Sig ) then + FSubscribers.Notify( + function( Subscriber: IMycSubscriber ): Boolean begin - Res := TMyc2Condition.Create; - - Sig.Advise( Res ); - - for j := i + 1 to Count - 1 do - begin - Sig := Getter( j ); - if not IsStatic( Sig ) then - Sig.Advise( Res ); - end; - - exit( Res.State ); - end; - end; - - exit( Null.State ); + Result := Subscriber.Notify; + end ); end; -class constructor TMyc2Signal.CreateClass; +function TMycSignal.Subscribe(const Subscriber: IMycSubscriber): TMycSubscription; begin - FNull := TMyc2NullSink.Create; -end; - -class destructor TMyc2Signal.DestroyClass; -begin - FNull := nil; -end; - -class function TMyc2Signal.IsStatic( const Signal: IMyc2Signal ): Boolean; -begin - Result := ( Signal = nil ) or ( Signal = FNull.State ); -end; - -class function TMyc2Signal.MakeValid( const Signal: IMyc2Signal ): IMyc2Signal; -begin - Result := Signal; - if Result = nil then - Result := Null.State; -end; - -function TMyc2Signal.Advise( const Sink: IMyc2Sink ): TSignalTag; -begin - if IsStatic( Sink ) then - exit( 0 ); - - FList.Lock; - try - Result := FList.Advise( Sink ); - - if IsSet then - Sink.Notify; - finally - FList.Release; - end; -end; - -class function TMyc2Signal.CreateConcat( const States: array of IMyc2State ): IMyc2State; - - function CreatePair( const A, B: IMyc2State ): IMyc2State; - var - Counter: IMyc2Condition; - begin - if A=B then - begin - Result := A; - end - else if IsStatic( A ) then - begin - if IsStatic( B ) then - Result := Signals.Null.State - else - Result := B; - end - else if IsStatic( B ) then - begin - Result := A; - end - else - begin - Counter := CreateCounter( 2 ); - A.Advise( Counter ); - B.Advise( Counter ); - Result := Counter.State; - end; - end; - - function CreateSet( const States: array of IMyc2State ): IMyc2State; - var - Counter: IMyc2Condition; - i, n: Integer; - begin - n := 0; - for i := 0 to High( States ) do - if not IsStatic( States[i] ) then - inc( n ); - - Counter := CreateCounter( n ); - for i := 0 to High( States ) do - if not IsStatic( States[i] ) then - States[i].Advise( Counter ); - - Result := Counter.State; - end; - -begin - {TODO -oMS: Potentielles Speicherloch. Im Gegensatz zur Union werden hier die States nicht explizit un-advised. Andererseits - kann man eigentlich davon ausgehen, das States im Gegensatz zu einfachen Notifications irgendwann gesetzt und dann aufgeräumt - werden. Im Auge behalten! } - - case Length( States ) of - 0: - Result := Signals.Null.State; - 1: - Result := States[0]; - 2: - Result := CreatePair( States[0], States[1] ); - else - Result := CreateSet( States ); - end; -end; - -class function TMyc2Signal.CreateCounter( Count: Integer ): IMyc2Condition; -begin - Assert( Count >=0 ); - - if Count > 0 then - Result := TMyc2CountSignal.Create( Count ) - else - Result := Null; -end; - -class function TMyc2Signal.CreateEvent( Init: Boolean ): IMyc2Event; -begin - Result := TMyc2Event.Create( Init ) -end; - -class function TMyc2Signal.CreateGroup( Count: Integer; const Getter: TFunc ): IMyc2Signal; -var - i, n: Integer; - Sigs: TArray; - Res: IMyc2Condition; -begin - if Count = 0 then - exit( Null.State ); - - SetLength( Sigs, Count ); - n := 0; - for i := 0 to Count - 1 do - begin - Sigs[n] := Getter( i ); - if not IsStatic( Sigs[n] ) then - inc( n ); - end; - - if n = 0 then - exit( Null.State ); - - Res := CreateCounter( n ); - for i := 0 to n - 1 do - Sigs[i].Advise( Res ); - - exit( Res.State ); -end; - -class function TMyc2Signal.CreateNotifier: IMyc2Notifier; -begin - Result := TMyc2Notifier.Create; -end; - -class function TMyc2Signal.MakeValid( const Signals: array of IMyc2Signal ): TArray; -var - i, n: Integer; -begin - n := 0; - for i := 0 to High( Signals ) do - if not IsStatic( Signals[i] ) then - inc( n ); - - SetLength( Result, n ); - n := 0; - for i := 0 to High( Signals ) do - if not IsStatic( Signals[i] ) then - begin - Result[n] := Signals[i]; - inc( n ); - end; -end; - -class function TMyc2Signal.CreateUnion( const Sigs: array of IMyc2Signal ): IMyc2Signal; -begin - case Length( Sigs ) of - 0: - Result := Signals.Null.State; - 1: - Result := Sigs[0]; - else - Result := TMycSignalUnion.Create( Sigs ); - end; -end; - -function TMyc2Signal.DoNotify: Boolean; -begin - FList.Notify; - exit( true ); -end; - -function TMyc2Signal.GetIsSet: Boolean; -begin - Result := true; -end; - -class function TMyc2Signal.IsStatic( const Sink: IMyc2Sink ): Boolean; -begin - Result := ( Sink = nil ) or ( Sink = FNull ); -end; - -function TMyc2Signal.Notify: Boolean; -begin - FList.Lock; - try - Result := DoNotify; - if not Result then - FList.Finalize; - finally - FList.Release; - end; -end; - -procedure TMyc2Signal.Unadvise( Tag: TSignalTag ); -begin - if Tag=0 then + if not Assigned(Subscriber) then exit; - FList.Lock; + FSubscribers.Lock; try - FList.Unadvise( Tag ); + Result.Setup( Self, FSubscribers.Advise( Subscriber ) ); finally - FList.Release; + FSubscribers.Release; end; end; -{ TMyc2Event } - -constructor TMyc2Event.Create( AInit: Boolean ); +procedure TMycSignal.Unlock; begin - inherited Create; - if AInit then - inc( FValue ); + end; -function TMyc2Event.GetIsSet: Boolean; +procedure TMycSignal.Unsubscribe(Tag: TMycNotifyList.TTag); begin - Result := FValue <> 0; + if Tag=nil then + exit; + + FSubscribers.Lock; + try + FSubscribers.Unadvise( Tag ); + finally + FSubscribers.Release; + end; end; -function TMyc2Event.DoNotify: Boolean; +procedure TMycSubscription.Setup(ASignal: TMycSignal; ATag: + TMycNotifyList.TTag); begin - if TInterlocked.Exchange( FValue, 1 ) = 0 then - inherited; - exit( true ); + FSignal := ASignal; + FTag := ATag; end; -function TMyc2Event.Reset: Boolean; +class operator TMycSubscription.Assign(var Dest: TMycSubscription; const [ref] + Src: TMycSubscription); begin - Result := TInterlocked.Exchange( FValue, 0 ) = 1; + Dest.Setup( Src.FSignal, Src.FTag ); end; -{ TMyc2CountownSignal } +class operator TMycSubscription.Initialize(out Dest: TMycSubscription); +begin + Dest.FTag := nil; +end; -constructor TMyc2CountSignal.Create( ACount: Integer ); +class operator TMycSubscription.Finalize(var Dest: TMycSubscription); +begin + if Dest.FTag=nil then + exit; + Dest.FSignal.Unsubscribe( Dest.FTag ); +end; + +constructor TMycSemaphore.Create(ACount: Integer); begin inherited Create; FCount := ACount; end; -function TMyc2CountSignal.GetIsSet: Boolean; +function TMycSemaphore.Notify: Boolean; +begin + Lock; + try + var n := TInterlocked.Decrement( FCount ); + if n < -MaxInt div 2 then + TInterlocked.Increment( FCount ); + + if n = 0 then + Notify; + + Result := n > 0; + if not Result then + Finalize; + finally + Release; + end; +end; + +function TMycSemaphore.GetIsSet: Boolean; begin Result := FCount <= 0; end; -function TMyc2CountSignal.DoNotify: Boolean; -var - n: Integer; +function TMycSemaphore.GetState: IMycState; begin - n := TInterlocked.Decrement( FCount ); - if n < -MaxInt div 2 then - TInterlocked.Increment( FCount ); - - if n = 0 then - inherited; - - Result := n > 0; + exit( Self ); end; -{ TMyc2SinkProc } +{ Signals } -constructor TMyc2SinkProc.Create( AOnce: Boolean; const AProc: TProc ); +class function Signals.CreateSemaphore(Count: Integer): IMycSemaphore; begin - inherited Create; - FProc := AProc; - FOnce := AOnce; -end; - -class function TMyc2SinkProc.CreateSink( Once: Boolean; const Proc: TProc ): IMyc2Sink; -begin - if Assigned( Proc ) then - Result := TMyc2SinkProc.Create( Once, Proc ) - else - Result := TMyc2Signal.Null; -end; - -function TMyc2SinkProc.Notify: Boolean; -begin - if Assigned( FProc ) then - begin - FProc( ); - if FOnce then - FProc := nil; - end; - - Result := Assigned( FProc ); -end; - -{ TMyc2NullState } - -function TMyc2NullState.Advise( const Sink: IMyc2Sink ): TSignalTag; -begin - Sink._AddRef; - try - Sink.Notify; - finally - Sink._Release; - end; - exit( 0 ); -end; - -function TMyc2NullState.GetIsSet: Boolean; -begin - exit( true ); -end; - -procedure TMyc2NullState.Unadvise( Tag: TSignalTag ); -begin -end; - -procedure TMyc2NullState.UnadviseAll; -begin - -end; - -constructor TMyc2NullSink.Create; -begin - inherited Create; - FState := TMyc2NullState.Create; -end; - -destructor TMyc2NullSink.Destroy; -begin - inherited Destroy; -end; - -function TMyc2NullSink.GetState: IMyc2State; -begin - Result := FState; -end; - -function TMyc2NullSink.Notify: Boolean; -begin - Result := false; -end; - -{ TMyc2Notifier } - -function TMyc2Notifier.GetSignal: IMyc2Signal; -begin - Result := Self; -end; - -{ TMyc2Condition } - -function TMyc2Condition.GetState: IMyc2State; -begin - Result := Self; -end; - -{ TMycSinkList } - -class constructor TMycSinkList.CreateClass; -begin - FItems := TMycAtomicStack.Create; - FPendingCnt := 0; - FUsedCnt := 0; -end; - -class destructor TMycSinkList.DestroyClass; -begin - Assert( FUsedCnt = 0 ); - Assert( FPendingCnt = 0 ); - - FItems.Free; -end; - -procedure TMycSinkList.Create; -begin - FFirst := 1; - FList := nil; - {$ifdef DEBUG_SIGNALS} - FCount := 0; - {$endif} -end; - -procedure TMycSinkList.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, 'Signal must not be locked!' ); - FFirst := FFirst and not 3; - UnadviseAll; -end; - -function TMycSinkList.Advise( const Sink: IMyc2Sink ): TSignalTag; -var - Item: PItem; -begin - Assert( IsLocked, 'Signal has to be locked' ); - - if IsFinalized then - exit( 0 ); - - {$ifdef DEBUG_SIGNALS} - inc( FCount ); - Assert( FCount < 40000 ); - {$endif} - if FFirst=0 then - begin - IMyc2Sink( FFirst ) := Sink; - exit( TSignalTag( Sink ) ); - end; - - Item := AllocItem; - Item.Sink := Sink; - Item.Prev := nil; - Item.Next := FList; - if Item.Next<>nil then - Item.Next.Prev := Item; - - FList := Item; - - exit( TSignalTag( Item ) ); -end; - -procedure TMycSinkList.Lock; -begin - repeat - if FFirst and 1 = 0 then - begin - YieldProcessor; - continue; - end; - until TInterlocked.BitTestAndClear( PInteger( @FFirst )^, 0 ); - - Assert( IsLocked, 'Locking failed' ); -end; - -procedure TMycSinkList.Notify; -var - Item, P: PItem; -begin - Assert( IsLocked, 'Signal has to be locked' ); - - if IsFinalized then - exit; - - if FFirst<>0 then - if not IMyc2Sink( FFirst ).Notify then - IMyc2Sink( FFirst ) := nil; - - Item := FList; - while Item<>nil do - begin - P := Item.Next; - if not Item.Sink.Notify then - Unadvise( TSignalTag( Item ) ); - Item := P; - end; -end; - -class function TMycSinkList.AllocItem: PItem; -begin - GetMem( Result, sizeof( TItem ) ); - Pointer( Result.Sink ) := nil; -end; - -procedure TMycSinkList.Finalize; -begin - Assert( IsLocked, 'Signal has to be locked' ); - UnadviseAll; - FFirst := FFirst or 2; -end; - -class procedure TMycSinkList.FreeItem( Item: PItem ); -begin - FreeMem( Item, sizeof( TItem ) ); -end; - -function TMycSinkList.IsLocked: Boolean; -begin - Result := FFirst and 1 = 0; -end; - -function TMycSinkList.IsFinalized: Boolean; -begin - Result := FFirst and 2 <> 0; -end; - -procedure TMycSinkList.Release; -begin - Assert( IsLocked, 'Signal not locked' ); - TInterlocked.Exchange( Pointer( FFirst ), Pointer( FFirst or 1 ) ); -end; - -procedure TMycSinkList.Unadvise( Tag: TSignalTag ); -var - Item: PItem; -begin - Assert( IsLocked, 'Signal not locked' ); - - if IsFinalized then - exit; - - {$ifdef DEBUG_SIGNALS} - dec( FCount ); - {$endif} - if Tag = FFirst then - begin - IMyc2Sink( FFirst ) := nil; - exit; - end; - - 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.Sink := nil; - - FreeItem( Item ); -end; - -procedure TMycSinkList.UnadviseAll; -begin - Assert( IsLocked, 'Signal has to be locked' ); - - if IsFinalized then - exit; - - IMyc2Sink( FFirst ) := nil; - while FList<>nil do - Unadvise( TSignalTag( FList ) ); -end; - -{ TMycObserver } - -class function TMycObserver.CreateObserver( const Signals: array of IMyc2Signal; const Item: T ): IMycObserver; -var - Arr: TArray; -begin - {$message hint 'Design-Fehler!! Der Null-Observer ist nicht statisch!!'} - if Length( Signals ) = 0 then - Result := TNull.Create( Item ) - else if ( Length( Signals )=1 ) and not TMyc2Signal.IsStatic( Signals[0] ) then - Result := TOne.Create( Signals[0], Item ) - else - begin - Arr := TMyc2Signal.MakeValid( Signals ); - if Arr = nil then - Result := TNull.Create( Item ) - else if Length( Arr )=1 then - Result := TOne.Create( Arr[0], Item ) - else - Result := TArr.Create( Arr, Item ); - end; -end; - -{ TMycObserver.TNull } - -constructor TMycObserver.TNull.Create( const ASink: T ); -begin - inherited Create; - FSink := ASink; -end; - -function TMycObserver.TNull.GetItem: T; -begin - Result := FSink; -end; - -{ TMycObserver.TOne } - -constructor TMycObserver.TOne.Create( const ASignal: IMyc2Signal; const ASink: T ); -begin - inherited Create( ASink ); - FSignal := ASignal; - FTag := FSignal.Advise( FSink ); -end; - -destructor TMycObserver.TOne.Destroy; -begin - FSignal.Unadvise( FTag ); - inherited; -end; - -{ TMycObserver.TArr } - -constructor TMycObserver.TArr.Create( const ASignals: TArray; const ASink: T ); -var - i: Integer; -begin - inherited Create( ASink ); - FSignals := ASignals; - - SetLength( FTags, Length( FSignals ) ); - for i := 0 to High( FTags ) do - begin - if not Signals.IsStatic( FSignals[i] ) then - FTags[i] := FSignals[i].Advise( FSink ) - else - FTags[i] := 0; - end; -end; - -destructor TMycObserver.TArr.Destroy; -var - i: Integer; -begin - for i := High( FSignals ) downto 0 do - if FTags[i]<>0 then - FSignals[i].Unadvise( FTags[i] ); - inherited; -end; - -{ TMycSignalUnion } - -constructor TMycSignalUnion.Create( const ASignals: array of IMyc2Signal ); -var - i: Integer; -begin - inherited Create; - - FNotifier := Signals.CreateNotifier; - SetLength( FTags, Length( ASignals ) ); - SetLength( FSignals, Length( ASignals ) ); - for i := 0 to High( FSignals ) do - begin - FSignals[i] := ASignals[i]; - FTags[i] := FSignals[i].Advise( FNotifier ); - end; -end; - -destructor TMycSignalUnion.Destroy; -var - i: Integer; -begin - for i := High( FTags ) downto 0 do - FSignals[i].Unadvise( FTags[i] ); - - inherited; -end; - -function TMycSignalUnion.Advise( const Sink: IMyc2Sink ): TSignalTag; -begin - Result := FNotifier.Signal.Advise( Sink ); -end; - -procedure TMycSignalUnion.Unadvise( Tag: TSignalTag ); -begin - FNotifier.Signal.Unadvise( Tag ); -end; - -{ TMyc2Flag } - -constructor TMyc2Flag.Create( AFlag: Boolean ); -begin - inherited Create; - FFlag := Integer( AFlag ); -end; - -function TMyc2Flag.Notify: Boolean; -begin - Result := TInterlocked.Exchange( FFlag, 1 ) = 0; -end; - -function TMyc2Flag.Reset: Boolean; -begin - Result := TInterlocked.Exchange( FFlag, 0 ) = 1; + Result := TMycSemaphore.Create( Count ); end; end. diff --git a/Test/MycTests.dpr b/Test/MycTests.dpr index cfb6fae..77a3ce2 100644 --- a/Test/MycTests.dpr +++ b/Test/MycTests.dpr @@ -15,9 +15,12 @@ uses DUnitX.Loggers.Xml.NUnit, {$ENDIF } DUnitX.TestFramework, - TestSignals in 'TestSignals.pas', + TestNotifier in 'TestNotifier.pas', TestSList in 'TestSList.pas', - TestStack in 'TestStack.pas'; + TestStack in 'TestStack.pas', + Myc.Core.Notifier in '..\Src\Myc.Core.Notifier.pas', + TestNotifier_Threading in 'TestNotifier_Threading.pas', + TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas'; { keep comment here to protect the following conditional from being removed by the IDE when adding a unit } {$IFNDEF TESTINSIGHT} diff --git a/Test/MycTests.dproj b/Test/MycTests.dproj index 84da01a..e8127c5 100644 --- a/Test/MycTests.dproj +++ b/Test/MycTests.dproj @@ -105,9 +105,12 @@ MainSource - + + + + Base diff --git a/Test/TestNotifier.pas b/Test/TestNotifier.pas new file mode 100644 index 0000000..b53a4c7 --- /dev/null +++ b/Test/TestNotifier.pas @@ -0,0 +1,262 @@ +unit TestNotifier; + +interface + +uses + DUnitX.TestFramework, + System.SysUtils, // For EAssertionFailed, EAccessViolation + Myc.Core.Notifier; + +type + // Dummy interface and class for testing + IMyTestInterface = interface( IInterface ) + ['{7A8C1A01-1C6B-4A7A-B9D8-28E6A8D02A0F}'] + // Unique GUID + procedure Foo; + function GetValue: Integer; + end; + + TMyTestReceiver = class( TInterfacedObject, IMyTestInterface ) + private + FValue: Integer; + FCallCount: Integer; + public + constructor Create( AValue: Integer ); + procedure Foo; + function GetValue: Integer; + property CallCount: Integer read FCallCount write FCallCount; + end; + + [TestFixture] + TMycTestNotifierTests = class + private + FNotifier: TMycNotifyList; + public + [Setup] + procedure Setup; + [TearDown] + procedure TearDown; + + [Test] + procedure Test01_DestroyExecutesWithoutErrors; + [Test] + procedure Test02_NotifyOnFinalizedLockedEmptyListExecutesWithoutErrors; + [Test] + procedure Test03_UnadviseAllOnFinalizedLockedEmptyListExecutesWithoutErrors; + [Test] + procedure Test04_AdviseUnadviseBasicFunctionality; + [Test] + procedure Test05_NotifyCallsAdvisedItemsAndCanRemoveThem; + end; + +implementation + +{ TMyTestReceiver } + +constructor TMyTestReceiver.Create( AValue: Integer ); +begin + inherited Create; + FValue := AValue; + FCallCount := 0; +end; + +procedure TMyTestReceiver.Foo; +begin + Inc( FCallCount ); +end; + +function TMyTestReceiver.GetValue: Integer; +begin + Result := FValue; +end; + +{ TMycTestNotifierTests } + +procedure TMycTestNotifierTests.Setup; +begin + FNotifier.Create; + // FNotifier's internal FFirst is now 1 (unlocked, not finalized) +end; + +procedure TMycTestNotifierTests.TearDown; +begin + // DUnitX creates a new test fixture instance for each test method. + // Setup re-initializes FNotifier. FNotifier is a record, so its lifetime is managed + // by the fixture. If it were an object managing external unmanaged resources, + // Destroy would be critical here. Given Destroy's issues, we rely on Setup. +end; + +// Test for: Correct execution of Destroy without assertion failures. +// With current buggy code, this test will FAIL due to EAssertionFailed in Destroy. +procedure TMycTestNotifierTests.Test01_DestroyExecutesWithoutErrors; +begin + // This call is expected to succeed without raising EAssertionFailed or other exceptions + // if TMycNotifyList.Destroy is implemented correctly. + FNotifier.Destroy; + Assert.IsTrue( True, 'FNotifier.Destroy completed without raising an exception.' ); +end; + +// Test for: Correct execution of Notify on an empty, locked, and finalized list. +// It should not cause an Access Violation or call the predicate. +// With current buggy code, this test will FAIL due to an Access Violation. +procedure TMycTestNotifierTests.Test02_NotifyOnFinalizedLockedEmptyListExecutesWithoutErrors; +var + predicateCallCount: Integer; + dummyPredicate: TPredicate; +begin + predicateCallCount := 0; + dummyPredicate := function( Item: IMyTestInterface ): Boolean + begin + Inc( predicateCallCount ); + Result := True; + end; + + FNotifier.Lock; + Assert.IsTrue( FNotifier.IsLocked, 'Notifier should be locked.' ); + + FNotifier.Finalize; // Internally, FFirst becomes 2 (locked & finalized state bits) if it was 0 after Lock + Assert.IsTrue( FNotifier.IsFinalized, 'Notifier should be finalized.' ); + + // In a corrected Notifier, this call should not cause an AV and not call the predicate. + FNotifier.Notify( dummyPredicate ); + Assert.AreEqual( 0, predicateCallCount, 'Notify predicate should not have been called for an empty, finalized list.' ); + Assert.IsTrue( True, 'FNotifier.Notify on finalized locked empty list completed without AV.' ); + + if FNotifier.IsLocked then // Release lock for subsequent operations or cleanup + begin + FNotifier.Release; + end; +end; + +// Test for: Correct execution of UnadviseAll on an empty, locked, and finalized list. +// It should not cause an Access Violation. +// With current buggy code, this test will FAIL due to an Access Violation. +procedure TMycTestNotifierTests.Test03_UnadviseAllOnFinalizedLockedEmptyListExecutesWithoutErrors; +begin + FNotifier.Lock; + Assert.IsTrue( FNotifier.IsLocked, 'Notifier should be locked.' ); + + FNotifier.Finalize; // FFirst becomes 2 internally + Assert.IsTrue( FNotifier.IsFinalized, 'Notifier should be finalized.' ); + + // In a corrected Notifier, this call should not cause an AV. + FNotifier.UnadviseAll; + Assert.IsTrue( True, 'FNotifier.UnadviseAll on finalized locked empty list completed without AV.' ); + + if FNotifier.IsLocked then // Release lock + begin + FNotifier.Release; + end; +end; + +// Test for: Basic Advise/Unadvise functionality (assumed to use FList). +// This test should PASS with the current code if FList handling is correct. +procedure TMycTestNotifierTests.Test04_AdviseUnadviseBasicFunctionality; +var + tag1, tag2: TMycNotifyList.TTag; + receiver1, receiver2: IMyTestInterface; +begin + receiver1 := TMyTestReceiver.Create( 1 ); + receiver2 := TMyTestReceiver.Create( 2 ); + + FNotifier.Lock; + try + tag1 := FNotifier.Advise( receiver1 ); + Assert.AreNotEqual( TMycNotifyList.TTag( nil ), tag1, 'Tag1 should not be nil.' ); + FNotifier.Unadvise( tag1 ); + + tag1 := FNotifier.Advise( receiver1 ); + Assert.AreNotEqual( TMycNotifyList.TTag( nil ), tag1, 'Tag1 should not be nil.' ); + + tag2 := FNotifier.Advise( receiver2 ); + Assert.AreNotEqual( TMycNotifyList.TTag( nil ), tag2, 'Tag2 should not be nil.' ); + Assert.AreNotEqual( tag1, tag2, 'Tag1 and Tag2 should be different.' ); + + FNotifier.Unadvise( tag1 ); + FNotifier.Unadvise( tag2 ); + + // Re-add and unadvise in different order + tag1 := FNotifier.Advise( receiver1 ); + tag2 := FNotifier.Advise( receiver2 ); + Assert.AreNotEqual( TMycNotifyList.TTag( nil ), tag1 ); + Assert.AreNotEqual( TMycNotifyList.TTag( nil ), tag2 ); + FNotifier.Unadvise( tag2 ); + FNotifier.Unadvise( tag1 ); + + finally + FNotifier.Release; + end; + Assert.IsFalse( FNotifier.IsLocked, 'Notifier should be unlocked.' ); +end; + +// Test for: Notify calls advised items and can remove items based on the predicate. +// This test should PASS with the current code if FList handling in Notify is correct. +procedure TMycTestNotifierTests.Test05_NotifyCallsAdvisedItemsAndCanRemoveThem; +var + rcv1, rcv2, rcv3: TMyTestReceiver; + itemsProcessedCount: Integer; +begin + rcv1 := TMyTestReceiver.Create( 10 ); // Keep + rcv2 := TMyTestReceiver.Create( 20 ); // Remove + rcv3 := TMyTestReceiver.Create( 30 ); // Keep + + FNotifier.Lock; + try + FNotifier.Advise( rcv1 ); + FNotifier.Advise( rcv2 ); + FNotifier.Advise( rcv3 ); + + itemsProcessedCount := 0; + FNotifier.Notify( + function( Item: IMyTestInterface ): Boolean + begin + Inc( itemsProcessedCount ); + TMyTestReceiver( Item ).Foo; + Result := Item.GetValue <> 20; // Remove item with value 20 + end + ); + + Assert.AreEqual( 3, itemsProcessedCount, 'Notify predicate called for all 3 items.' ); + Assert.AreEqual( 1, rcv1.CallCount, 'Receiver1 called once.' ); + Assert.AreEqual( 1, rcv2.CallCount, 'Receiver2 called once (before removal).' ); + Assert.AreEqual( 1, rcv3.CallCount, 'Receiver3 called once.' ); + + // Reset counts and check again + rcv1.CallCount := 0; + rcv2.CallCount := 0; // Should remain 0 as it's removed + rcv3.CallCount := 0; + itemsProcessedCount := 0; + + FNotifier.Notify( + function( Item: IMyTestInterface ): Boolean + begin + Inc( itemsProcessedCount ); + TMyTestReceiver( Item ).Foo; + Result := True; // Keep remaining + end + ); + Assert.AreEqual( 2, itemsProcessedCount, 'Notify predicate called for 2 remaining items.' ); + Assert.AreEqual( 1, rcv1.CallCount, 'Receiver1 called again.' ); + Assert.AreEqual( 0, rcv2.CallCount, 'Receiver2 (removed) not called again.' ); + Assert.AreEqual( 1, rcv3.CallCount, 'Receiver3 called again.' ); + + finally + FNotifier.Release; + end; + + // Cleanup remaining items (rcv1, rcv3) + FNotifier.Lock; + try + // This UnadviseAll should work on the remaining FList items. + // If the list becomes empty in a way that FFirst is misconfigured, it might still fail. + FNotifier.UnadviseAll; + finally + FNotifier.Release; + end; +end; + +initialization + +TDUnitX.RegisterTestFixture( TMycTestNotifierTests ); + +end. diff --git a/Test/TestNotifier_ChaosStress.pas b/Test/TestNotifier_ChaosStress.pas new file mode 100644 index 0000000..fd54a45 --- /dev/null +++ b/Test/TestNotifier_ChaosStress.pas @@ -0,0 +1,424 @@ +unit TestNotifier_ChaosStress; + +interface + +uses + System.SysUtils, + System.Classes, // For TThread + System.SyncObjs, // For TInterlocked, TCriticalSection (needed by implementation) + System.Generics.Collections, // For TList + System.Diagnostics, // For TStopwatch + DUnitX.TestFramework, + Myc.Core.Notifier; // Your unit + +type + IMyStressTestInterface = interface(IInterface) + ['{B8A3F7E1-47D9-4A9C-8C2B-035A1B568E7F}'] + procedure Foo(NotificationType: Integer); // NotificationType for more detailed testing + function GetValue: Integer; + function GetNotificationCount: Integer; + function GetInstanceID: Integer; + function GetExpectedToBeAdvised: Boolean; + procedure SetExpectedToBeAdvised(const Value: Boolean); + property ExpectedToBeAdvised: Boolean read GetExpectedToBeAdvised write + SetExpectedToBeAdvised; + end; + + TMyStressReceiver = class(TInterfacedObject, IMyStressTestInterface) + private + FInstanceID: Integer; + FValue: Integer; + FNotificationCount: Integer; + FExpectedToBeAdvised: Boolean; + function GetExpectedToBeAdvised: Boolean; + procedure SetExpectedToBeAdvised(const Value: Boolean); + public + // For testing: flag to indicate if the thread that created this believes it's still advised + constructor Create(AInstanceID, AValue: Integer); + procedure Foo(NotificationType: Integer); + function GetValue: Integer; + function GetNotificationCount: Integer; + function GetInstanceID: Integer; + property ExpectedToBeAdvised: Boolean read GetExpectedToBeAdvised write + SetExpectedToBeAdvised; + end; + + // Record type to store an advised receiver and its tag + TAdvisedReceiverInfo = record + Tag: TMycNotifyList.TTag; + Receiver: IMyStressTestInterface; + end; + + TMycNotifierChaosStressTests = class; // Forward declaration + + TStressWorkerThread = class(TThread) + private + FOwnerFixture: TMycNotifierChaosStressTests; + FThreadID: Integer; + FIterations: Integer; + FError: Exception; + // List of receivers this thread has successfully advised and believes are still active + FMyAdvisedReceivers: TList; + protected + procedure Execute; override; + public + constructor Create(AOwnerFixture: TMycNotifierChaosStressTests; AThreadID, AIterations: Integer); + destructor Destroy; override; + property Error: Exception read FError; + function GetAdvisedReceiverCountByThread: Integer; + // procedure GetMyExpectedLiveReceivers(AList: TList); // Not strictly needed with current verification + end; + + [TestFixture] + TMycNotifierChaosStressTests = class(TObject) + public + FNotifier: TMycNotifyList; + // List of all receivers ever created in this test run, for final verification + FAllReceiversCreated: TList; + FCriticalSectionForList: TCriticalSection; // To protect FAllReceiversCreated + FNextReceiverInstanceID: Integer; // Atomically incremented ID for receivers + + [Setup] + procedure Setup; + [TearDown] + procedure TearDown; + + [Test] + procedure Test_MixedOperations_Stress; + end; + +implementation + +// uses System.StrUtils; // Not needed anymore if IfThen is not used + +{ TMyStressReceiver } +constructor TMyStressReceiver.Create(AInstanceID, AValue: Integer); +begin + inherited Create; + FInstanceID := AInstanceID; + FValue := AValue; + FNotificationCount := 0; + FExpectedToBeAdvised := False; // Initially not advised or expecting to be +end; + +procedure TMyStressReceiver.Foo(NotificationType: Integer); +begin + TInterlocked.Increment(FNotificationCount); // Atomic increment +end; + +function TMyStressReceiver.GetExpectedToBeAdvised: Boolean; +begin + Result := FExpectedToBeAdvised; +end; + +function TMyStressReceiver.GetValue: Integer; +begin + Result := FValue; +end; + +function TMyStressReceiver.GetNotificationCount: Integer; +begin + // Atomic read, ensuring visibility of changes from other threads if FNotificationCount was public + // For private field accessed by owner, direct read is fine if Notify is serialized. + // TInterlocked.Add(var Target, Value) returns Target+Value. For read: Target+0. + Result := TInterlocked.Add(FNotificationCount, 0); +end; + +function TMyStressReceiver.GetInstanceID: Integer; +begin + Result := FInstanceID; +end; + +procedure TMyStressReceiver.SetExpectedToBeAdvised(const Value: Boolean); +begin + FExpectedToBeAdvised := Value; +end; + +{ TStressWorkerThread } +constructor TStressWorkerThread.Create(AOwnerFixture: TMycNotifierChaosStressTests; AThreadID, AIterations: Integer); +begin + inherited Create(True); // Creates the thread in a suspended state + FreeOnTerminate := False; // The test fixture will free the thread + FOwnerFixture := AOwnerFixture; + FThreadID := AThreadID; + FIterations := AIterations; + FError := nil; + FMyAdvisedReceivers := TList.Create; +end; + +destructor TStressWorkerThread.Destroy; +begin + FMyAdvisedReceivers.Free; + inherited; +end; + +procedure TStressWorkerThread.Execute; +var + i, op: Integer; + newReceiver: TMyStressReceiver; + tag: TMycNotifyList.TTag; + itemIndex: Integer; + advisedItemRec: TAdvisedReceiverInfo; + localInstanceID: Integer; +begin + inherited; // Important for TThread + try + for i := 1 to FIterations do + begin + if Terminated then Break; // Respond to termination request + + op := Random(100); + + if op < 35 then // 35% chance to Advise + begin + localInstanceID := TInterlocked.Increment(FOwnerFixture.FNextReceiverInstanceID); + newReceiver := TMyStressReceiver.Create(localInstanceID, FThreadID * 10000 + i); + // newReceiver.ExpectedToBeAdvised is False by default, will be set to True by this thread upon successful advise. + + FOwnerFixture.FCriticalSectionForList.Enter; + try + FOwnerFixture.FAllReceiversCreated.Add(newReceiver); + finally + FOwnerFixture.FCriticalSectionForList.Leave; + end; + + FOwnerFixture.FNotifier.Lock; + try + tag := FOwnerFixture.FNotifier.Advise(newReceiver); + advisedItemRec.Tag := tag; + advisedItemRec.Receiver := newReceiver; + FMyAdvisedReceivers.Add(advisedItemRec); + newReceiver.ExpectedToBeAdvised := True; // This thread now expects it to be advised + finally + FOwnerFixture.FNotifier.Release; + end; + end + else if (op < 70) and (FMyAdvisedReceivers.Count > 0) then // 35% chance to Unadvise (if possible) + begin + itemIndex := Random(FMyAdvisedReceivers.Count); + advisedItemRec := FMyAdvisedReceivers[itemIndex]; + + FOwnerFixture.FNotifier.Lock; + try + FOwnerFixture.FNotifier.Unadvise(advisedItemRec.Tag); + advisedItemRec.Receiver.ExpectedToBeAdvised := False; // This thread no longer expects it to be advised + FMyAdvisedReceivers.Delete(itemIndex); + finally + FOwnerFixture.FNotifier.Release; + end; + end + else // 30% chance to Notify + begin + FOwnerFixture.FNotifier.Lock; + try + if not FOwnerFixture.FNotifier.IsFinalized then // Don't notify if finalized + begin + FOwnerFixture.FNotifier.Notify( + function(Item: IMyStressTestInterface): Boolean + begin + Item.Foo(FThreadID); // Pass ThreadID as notification type for context + Result := True; // Keep item + end); + end; + finally + FOwnerFixture.FNotifier.Release; + end; + end; + + if (i mod 75 = 0) then Sleep(0); // Yield occasionally to encourage context switching + end; + except + on E: Exception do + FError := E; // Store the first error that occurred in the thread + end; +end; + +function TStressWorkerThread.GetAdvisedReceiverCountByThread: Integer; +begin + Result := FMyAdvisedReceivers.Count; +end; + +// Not strictly needed if main test iterates FAllReceiversCreated and checks ExpectedToBeAdvised flag +// procedure TStressWorkerThread.GetMyExpectedLiveReceivers(AList: TList); +// var +// itemRec: TAdvisedReceiverInfo; +// begin +// AList.Clear; +// for itemRec in FMyAdvisedReceivers do +// begin +// AList.Add(itemRec.Receiver); +// end; +// end; + + +{ TMycNotifierChaosStressTests } +procedure TMycNotifierChaosStressTests.Setup; +begin + Randomize; // Initialize random number generator + FNotifier.Create; + FAllReceiversCreated := TList.Create; + FCriticalSectionForList := TCriticalSection.Create; + FNextReceiverInstanceID := 0; +end; + +procedure TMycNotifierChaosStressTests.TearDown; +var + // receiver: TMyStressReceiver; // Not used directly in this simplified TearDown + i: Integer; +begin + // Attempt to shut down the Notifier cleanly. + try + FNotifier.Lock; // Lock for the cleanup process + try + // UnadviseAll should be safe if the IsFinalized guard is present + // and the FFirst state is not pathological. + if not FNotifier.IsFinalized then + begin + FNotifier.UnadviseAll; + end; + finally + if FNotifier.IsLocked then // Only release if the lock is still held + FNotifier.Release; + end; + except + // Suppress errors during teardown to avoid interfering with other tests. + // In a real test environment, this would be logged. + end; + + // FNotifier is a record field of the fixture and is "destroyed" with it when the fixture is destroyed. + // Setup reinitializes it for each test. + + if Assigned(FAllReceiversCreated) then + begin + // ARC will handle TMyStressReceiver instances when the list is freed/cleared, + // as long as no other strong references exist. + // Setting list items to nil is good practice if the list itself isn't immediately freed. + for i := 0 to FAllReceiversCreated.Count - 1 do + FAllReceiversCreated[i] := nil; // Break reference cycles if any, allow ARC + FAllReceiversCreated.Free; + FAllReceiversCreated := nil; + end; + if Assigned(FCriticalSectionForList) then + begin + FCriticalSectionForList.Free; + FCriticalSectionForList := nil; + end; +end; + +procedure TMycNotifierChaosStressTests.Test_MixedOperations_Stress; +const + NumThreads = 24; // Number of concurrent worker threads + IterationsPerThread = 5000; // Number of random operations per thread +var + threads: array of TStressWorkerThread; + i: Integer; + LThreadError: Exception; + LMessage: string; + totalExpectedLiveByThreadsAtEnd, actualLiveInNotifierAtEnd: Integer; + actualLiveReceiversInNotifier: TList; // Store interface type from Notify + receiver: IMyStressTestInterface; + found: Boolean; + stopwatch: TStopwatch; +begin + stopwatch := TStopwatch.StartNew; + + SetLength(threads, NumThreads); + for i := 0 to NumThreads - 1 do + threads[i] := TStressWorkerThread.Create(Self, i + 1, IterationsPerThread); + + for i := 0 to NumThreads - 1 do + threads[i].Start; + + totalExpectedLiveByThreadsAtEnd := 0; + for i := 0 to NumThreads - 1 do + begin + threads[i].WaitFor; + LThreadError := threads[i].Error; + if Assigned(LThreadError) then + begin + // If an error occurred, Assert.IsNull would fail. + // The message can safely use LThreadError.Message here. + Assert.IsNull(LThreadError, + 'Thread ' + threads[i].FThreadID.ToString + ' reported an error: ' + LThreadError.Message); + end + else + begin + // If no error, Assert.IsNull will pass. No custom message needed for success. + Assert.IsNull(LThreadError); + end; + totalExpectedLiveByThreadsAtEnd := totalExpectedLiveByThreadsAtEnd + threads[i].GetAdvisedReceiverCountByThread; + threads[i].Free; + end; + SetLength(threads, 0); + + // --- Verification Phase --- + + // 1. Get all items actually live in the notifier + actualLiveReceiversInNotifier := TList.Create; + try + FNotifier.Lock; + try + if not FNotifier.IsFinalized then + begin + FNotifier.Notify( + function(Item: IMyStressTestInterface): Boolean + begin + actualLiveReceiversInNotifier.Add(Item); + Result := True; + end); + end; + finally + FNotifier.Release; + end; + actualLiveInNotifierAtEnd := actualLiveReceiversInNotifier.Count; + + // 2. Compare overall counts + Assert.AreEqual(totalExpectedLiveByThreadsAtEnd, actualLiveInNotifierAtEnd, + Format('Mismatch in live item count at end. Threads expected %d, Notifier has %d.', [totalExpectedLiveByThreadsAtEnd, actualLiveInNotifierAtEnd])); + + // 3. Detailed check: Iterate all receivers ever created. + // Their 'ExpectedToBeAdvised' flag (last known state from its managing thread) + // should match their presence in 'actualLiveReceiversInNotifier'. + FCriticalSectionForList.Enter; + try + for receiver in FAllReceiversCreated do + begin + // Check if this receiver (by instance) is in the list of interfaces retrieved from Notify + found := False; + for var liveIntf in actualLiveReceiversInNotifier do + begin + if liveIntf = receiver then // Compare object instances + begin + found := True; + Break; + end; + end; + + Assert.AreEqual(receiver.ExpectedToBeAdvised, found, + Format('Receiver ID %d: Thread expected it to be advised=%d, but its presence in Notifier is %d. NotificationCount=%d', + [receiver.GetInstanceID, Integer(receiver.ExpectedToBeAdvised), Integer(found), receiver.GetNotificationCount])); + + // Further checks on NotificationCount could be added if specific notification patterns were expected. + // For this chaos test, ensuring count is non-negative and consistent with advised state is a good start. + Assert.IsTrue(receiver.GetNotificationCount >= 0, + Format('Receiver ID %d has non-positive notification count: %d', [receiver.GetInstanceID, receiver.GetNotificationCount])); + if found and (receiver.GetNotificationCount = 0) then + begin + // This might be okay if Notify calls were very sparse or the item was just added and not yet notified. + // For a long running stress test, one might expect > 0 if Notifies happened while it was live. + // Log.Debug(Format('Receiver ID %d found in notifier but has 0 notifications.', [receiver.GetInstanceID])); + end; + end; + finally + FCriticalSectionForList.Leave; + end; + finally + actualLiveReceiversInNotifier.Free; + end; + + stopwatch.Stop; +end; + +initialization + TDUnitX.RegisterTestFixture(TMycNotifierChaosStressTests); +end. diff --git a/Test/TestNotifier_Threading.pas b/Test/TestNotifier_Threading.pas new file mode 100644 index 0000000..3d120f8 --- /dev/null +++ b/Test/TestNotifier_Threading.pas @@ -0,0 +1,302 @@ +unit TestNotifier_Threading; + +interface + +uses + System.SysUtils, + System.Classes, // For TThread + System.Generics.Collections, + DUnitX.TestFramework, + Myc.Core.Notifier; // Your unit + +type + // The previously known test interface and implementation + IMyTestInterface = interface(IInterface) + ['{7A8C1A01-1C6B-4A7A-B9D8-28E6A8D02A0F}'] + procedure Foo; + function GetValue: Integer; + end; + + TMyTestReceiver = class(TInterfacedObject, IMyTestInterface) + private + FValue: Integer; + FCallCount: Integer; + public + constructor Create(AValue: Integer); + procedure Foo; + function GetValue: Integer; + property CallCount: Integer read FCallCount write FCallCount; + end; + + // Forward declaration for the test fixture class + TMycNotifierThreadingTests = class; + + // Helper thread class for concurrent Advise operations + TAdviseWorkerThread = class(TThread) + private + FOwnerFixture: TMycNotifierThreadingTests; // Reference to the test fixture for accessing FNotifier + FReceiversToAdd: array of IMyTestInterface; + FError: Exception; + FStartIndex: Integer; + FItemsPerThread: Integer; + protected + procedure Execute; override; + public + constructor Create(AOwnerFixture: TMycNotifierThreadingTests; AStartIndex, AItemsPerThread: Integer); + destructor Destroy; override; + property Error: Exception read FError; + end; + + [TestFixture] + TMycNotifierThreadingTests = class(TObject) + public + FNotifier: TMycNotifyList; // The shared Notifier instance + FReceiverMasterList: TList; // To hold references to all created receivers + + [Setup] + procedure Setup; + [TearDown] + procedure TearDown; + + [Test] + procedure Test_ConcurrentAdvise_Then_UnadviseAll_Consistency; + end; + +implementation + +uses + System.StrUtils; + +{ TMyTestReceiver } + +constructor TMyTestReceiver.Create(AValue: Integer); +begin + inherited Create; + FValue := AValue; + FCallCount := 0; +end; + +procedure TMyTestReceiver.Foo; +begin + Inc(FCallCount); +end; + +function TMyTestReceiver.GetValue: Integer; +begin + Result := FValue; +end; + +{ TAdviseWorkerThread } + +constructor TAdviseWorkerThread.Create(AOwnerFixture: TMycNotifierThreadingTests; AStartIndex, AItemsPerThread: Integer); +var + i: Integer; +begin + inherited Create(True); // Creates the thread in a suspended state + FreeOnTerminate := False; // The test fixture will free the thread + FOwnerFixture := AOwnerFixture; + FStartIndex := AStartIndex; + FItemsPerThread := AItemsPerThread; + FError := nil; + + SetLength(FReceiversToAdd, FItemsPerThread); + for i := 0 to FItemsPerThread - 1 do + begin + FReceiversToAdd[i] := TMyTestReceiver.Create(FStartIndex + i); + end; +end; + +destructor TAdviseWorkerThread.Destroy; +begin + // Releases the references to the receivers (ARC takes care of the objects) + SetLength(FReceiversToAdd, 0); + inherited Destroy; +end; + +procedure TAdviseWorkerThread.Execute; +var + i: Integer; + tag: TMycNotifyList.TTag; // Tag is not used further in this test design +begin + inherited; // Important for TThread + try + for i := 0 to High(FReceiversToAdd) do + begin + if Terminated then + Break; // Respond to termination request + + // Acquire lock before calling Advise + FOwnerFixture.FNotifier.Lock; + try + tag := FOwnerFixture.FNotifier.Advise(FReceiversToAdd[i]); + // The 'tag' could be used here if necessary for other test scenarios + finally + // Definitely release lock in the finally block + FOwnerFixture.FNotifier.Release; + end; + + // A short Sleep(Random(X)) could increase the probability here + // of thread interleaving issues, but makes the test less deterministic. + // Sleep(0); // or + if Random(10) < 2 then + Sleep(Random(2)); + end; + except + on E: Exception do + begin + FError := E; // Store the first error that occurred in the thread + end; + end; +end; + +{ TMycNotifierThreadingTests } + +procedure TMycNotifierThreadingTests.Setup; +begin + Randomize; + FNotifier.Create; + FReceiverMasterList := TList.Create; +end; + +procedure TMycNotifierThreadingTests.TearDown; +var + i: Integer; +begin + // Attempt to shut down the Notifier cleanly. + // This assumes that FNotifier is not irreparably damaged. + try + FNotifier.Lock; // Lock for the cleanup process + try + // UnadviseAll should be safe if the IsFinalized guard is present + // and the FFirst state is not pathological. + if not FNotifier.IsFinalized then + begin + FNotifier.UnadviseAll; + end; + finally + if FNotifier.IsLocked then // Only release if the lock is still held + FNotifier.Release; + end; + except + // Suppress errors during teardown to avoid interfering with other tests. + // In a real test environment, this would be logged. + end; + + // FNotifier is a record field of the fixture and is "destroyed" with it. + // Setup reinitializes it for each test. + + if Assigned(FReceiverMasterList) then + begin + // Release references in the master list (ARC takes care of the objects) + for i := 0 to FReceiverMasterList.Count - 1 do + FReceiverMasterList[i] := nil; + FReceiverMasterList.Clear; + FReceiverMasterList.Free; + FReceiverMasterList := nil; + end; +end; + +procedure TMycNotifierThreadingTests.Test_ConcurrentAdvise_Then_UnadviseAll_Consistency; +const + NumThreads = 24; // Number of threads + ItemsPerThread = 2500; // Number of Advise operations per thread +var + threads: array of TAdviseWorkerThread; + i: Integer; + totalAdvisedExpected: Integer; + currentNotifyCount: Integer; + receiver: IMyTestInterface; + LThreadError: Exception; // For safe error message handling + LMessage: string; // For safe error message handling +begin + totalAdvisedExpected := NumThreads * ItemsPerThread; + SetLength(threads, NumThreads); + + // Create threads + for i := 0 to NumThreads - 1 do + begin + threads[i] := TAdviseWorkerThread.Create(Self, i * ItemsPerThread, ItemsPerThread); + end; + + // Start all threads + for i := 0 to NumThreads - 1 do + begin + threads[i].Start; + end; + + // Wait for all threads to complete and check for errors + for i := 0 to NumThreads - 1 do + begin + threads[i].WaitFor; + + LThreadError := threads[i].Error; + if Assigned(LThreadError) then + begin + LMessage := 'Thread ' + i.ToString + ' reported an error: ' + LThreadError.Message; + Assert.IsNull(LThreadError, LMessage); // This will fail and show the message + end + else + begin + Assert.IsNull(LThreadError); // This will pass + end; + + // Add the receivers added by this thread to the MasterList, + // to ensure their lifetime for the duration of the test. + for receiver in threads[i].FReceiversToAdd do + begin + FReceiverMasterList.Add(receiver); + end; + threads[i].Free; // Free thread object + end; + SetLength(threads, 0); + + // 1. Consistency check: Number of items in the Notifier after all Advise operations. + // Since TMycNotifyList does not have a direct Count property, we count via Notify. + currentNotifyCount := 0; + FNotifier.Lock; + try + FNotifier.Notify( + function(Item: IMyTestInterface): Boolean + begin + Inc(currentNotifyCount); + Result := True; // Keep item in the Notifier + end + ); + finally + FNotifier.Release; + end; + Assert.AreEqual(totalAdvisedExpected, currentNotifyCount, + 'The number of items in the Notifier after all Advise operations (' + currentNotifyCount.ToString + + ') does not match the expected number (' + totalAdvisedExpected.ToString + ').'); + + // 2. Execute UnadviseAll + FNotifier.Lock; + try + FNotifier.UnadviseAll; // Should remove all items + finally + FNotifier.Release; + end; + + // 3. Consistency check: Notifier should be empty after UnadviseAll. + currentNotifyCount := 0; + FNotifier.Lock; + try + FNotifier.Notify( + function(Item: IMyTestInterface): Boolean + begin + Inc(currentNotifyCount); + Result := True; + end + ); + finally + FNotifier.Release; + end; + Assert.AreEqual(0, currentNotifyCount, + 'The Notifier should be empty after UnadviseAll, but still contains ' + currentNotifyCount.ToString + ' items.'); +end; + +initialization + +TDUnitX.RegisterTestFixture(TMycNotifierThreadingTests); + +end. diff --git a/Test/TestSignals.pas b/Test/TestSignals.pas index 046a9ce..e10e7fb 100644 --- a/Test/TestSignals.pas +++ b/Test/TestSignals.pas @@ -3,7 +3,8 @@ unit TestSignals; interface uses - DUnitX.TestFramework; + DUnitX.TestFramework, + Myc.Signals; type [TestFixture]