Refactoring

This commit is contained in:
Michael Schimmel
2025-06-02 12:27:52 +02:00
parent 86e2729a52
commit 762e7f83e2
8 changed files with 294 additions and 182 deletions
+80
View File
@@ -0,0 +1,80 @@
unit Myc.Core.Futures;
interface
uses
System.SysUtils,
Myc.Signals, Myc.TaskManager, Myc.Futures;
type
// Abstract base class for IMycFuture<T> implementations.
TMycFuture<T> = class abstract( TInterfacedObject, IMycFuture<T> )
protected
function GetResult: T; virtual; abstract;
function GetDone: IMycState; virtual; abstract;
end;
// Concrete future implementation triggered by an initial state, executing a TFunc<T>.
TMycInitStateFuncFuture<T> = class( TMycFuture<T> )
private
FInit: TMycSubscription;
FDone: IMycLatch;
FResult: T;
protected
function GetResult: T; override;
function GetDone: IMycState; override;
public
// Creates a future that executes AProc after AGate is set, using ATaskFactory.
constructor Create( const ATaskManager: IMycTaskManager; const AGate: IMycState; AProc: TFunc<T> );
destructor Destroy; override;
end;
implementation
{ TMycInitStateFuncFuture<T> }
constructor TMycInitStateFuncFuture<T>.Create( const ATaskManager: IMycTaskManager; const AGate: IMycState; AProc: TFunc<T> );
begin
inherited Create;
FDone := TMycLatch.CreateLatch( 1 );
// Subscribe the job execution to AGate.
// The job will run when AGate notifies the subscriber returned by Run.
FInit := ATaskManager.CreateTask(
AGate,
procedure
begin
try
try
Self.FResult := AProc( );
except
Self.FResult := Default ( T ); // Set result to Default(T) on error
raise; // Re-raise for TaskFactory to handle
end;
finally
Self.FDone.Notify; // Signal that this future is done (successfully or with error)
end;
end );
end;
destructor TMycInitStateFuncFuture<T>.Destroy;
begin
Assert( FDone.State.IsSet );
FInit.Unsubscribe;
inherited Destroy;
end;
function TMycInitStateFuncFuture<T>.GetDone: IMycState;
begin
Result := FDone.State;
end;
function TMycInitStateFuncFuture<T>.GetResult: T;
begin
Assert( FDone.State.IsSet, 'Result is not yet available.' );
Result := FResult;
end;
end.
+37 -4
View File
@@ -4,10 +4,10 @@ interface
uses uses
System.SysUtils, System.Classes, System.SyncObjs, System.SysUtils, System.Classes, System.SyncObjs,
Myc.Core.Atomic, Myc.Signals; // Myc.Signals now requires direct use of TMycLatch static members Myc.Core.Atomic, Myc.TaskManager, Myc.Signals; // Myc.Signals now requires direct use of TMycLatch static members
type type
IMycTaskFactory = interface IMycTaskFactory = interface( IMycTaskManager )
{$region 'property access'} {$region 'property access'}
// Retrieves the number of worker threads. // Retrieves the number of worker threads.
function GetThreadCount: Integer; function GetThreadCount: Integer;
@@ -52,7 +52,7 @@ type
property ThreadCount: Integer read GetThreadCount; property ThreadCount: Integer read GetThreadCount;
end; end;
TMycTaskFactory = class(TInterfacedObject, IMycTaskFactory) TMycTaskFactory = class(TInterfacedObject, IMycTaskManager, IMycTaskFactory)
type type
ETaskException = class(Exception) end; ETaskException = class(Exception) end;
private private
@@ -67,6 +67,8 @@ type
FWorkStack: TMycAtomicStack<TProc>; // Stack of pending jobs FWorkStack: TMycAtomicStack<TProc>; // Stack of pending jobs
FWorkThreads: TArray<TThread>; // Array of worker threads FWorkThreads: TArray<TThread>; // Array of worker threads
procedure WorkerThread; procedure WorkerThread;
class var
FTaskManagerLock: Integer;
protected protected
procedure ExecuteJob; procedure ExecuteJob;
@@ -81,10 +83,14 @@ type
procedure HandleException; procedure HandleException;
procedure EnqueueJob(const Job: TProc); procedure EnqueueJob(const Job: TProc);
function Run(Job: TProc): IMycSubscriber; function Run(Job: TProc): IMycSubscriber;
function CreateTask(const Gate: IMycState; const Proc: TProc): TMycSubscription;
procedure WaitFor(State: IMycState); procedure WaitFor(State: IMycState);
procedure Teardown; procedure Teardown;
function InMainThread: Boolean; function InMainThread: Boolean;
function InWorkerThread: Boolean; function InWorkerThread: Boolean;
class procedure AquireTaskManager;
class procedure ReleaseTaskManager;
end; end;
implementation implementation
@@ -175,6 +181,20 @@ begin
Result.NameThreadForDebugging(DbgName); // Set thread name for debugging Result.NameThreadForDebugging(DbgName); // Set thread name for debugging
end; end;
function TMycTaskFactory.CreateTask(const Gate: IMycState; const Proc: TProc):
TMycSubscription;
begin
if not Assigned(Gate) or Gate.IsSet then
begin
EnqueueJob( Proc );
end
else
begin
// The job will run when Gate notifies the subscriber returned by Run.
Result := Gate.Subscribe( Run( Proc ) );
end;
end;
function TMycTaskFactory.CreateThread(const Proc: TProc): IMycState; function TMycTaskFactory.CreateThread(const Proc: TProc): IMycState;
var var
res: IMycLatch; res: IMycLatch;
@@ -261,6 +281,20 @@ begin
exit(false); // Not found in worker thread list exit(false); // Not found in worker thread list
end; end;
class procedure TMycTaskFactory.AquireTaskManager;
begin
if not Assigned(TaskManager) then
TaskManager := TMycTaskFactory.Create;
inc( FTaskManagerLock );
end;
class procedure TMycTaskFactory.ReleaseTaskManager;
begin
dec( FTaskManagerLock );
if FTaskManagerLock = 0 then
TaskManager := nil;
end;
function TMycTaskFactory.Run(Job: TProc): IMycSubscriber; function TMycTaskFactory.Run(Job: TProc): IMycSubscriber;
begin begin
if FTerminated <> 0 then if FTerminated <> 0 then
@@ -361,7 +395,6 @@ end;
function TMycPendingJob.Notify: Boolean; function TMycPendingJob.Notify: Boolean;
var var
newCount: Integer;
P: TProc; P: TProc;
begin begin
PPointer(@P)^ := TInterlocked.Exchange( PPointer(@FJob)^, nil ); PPointer(@P)^ := TInterlocked.Exchange( PPointer(@FJob)^, nil );
+36 -69
View File
@@ -4,7 +4,7 @@ interface
uses uses
System.SysUtils, System.SysUtils,
Myc.Signals, Myc.Core.Tasks; Myc.Signals, Myc.TaskManager;
type type
// Represents the eventual result of an asynchronous operation. // Represents the eventual result of an asynchronous operation.
@@ -19,91 +19,58 @@ type
property Done: IMycState read GetDone; property Done: IMycState read GetDone;
end; end;
// Abstract base class for IMycFuture<T> implementations. Future<T> = record
TMycFuture<T> = class abstract(TInterfacedObject, IMycFuture<T>) strict private
protected FFuture: IMycFuture<T>;
function GetResult: T; virtual; abstract; class constructor CreateClass;
function GetDone: IMycState; virtual; abstract; class destructor DestroyClass;
end; constructor Create(const AFuture: IMycFuture<T>);
public
class function Construct(const Proc: TFunc<T>): Future<T>; overload; static;
class function Construct(const Gate: IMycState; const Proc: TFunc<T>): Future<T>; overload; static;
// Concrete future implementation triggered by an initial state, executing a TFunc<T>. function Chain<S>(const Proc: TFunc<T, S>): Future<S>;
TMycInitStateFuncFuture<T> = class(TMycFuture<T>)
private
FInit: TMycSubscription;
FDone: IMycLatch;
FResult: T;
protected
function GetResult: T; override;
function GetDone: IMycState; override;
public
// Creates a future that executes AProc after AInitState is set, using ATaskFactory.
constructor Create(const ATaskFactory: IMycTaskFactory; const AInitState:
IMycState; AProc: TFunc<T>);
// Cleans up resources, primarily by unsubscribing from the initial state.
destructor Destroy; override;
end; end;
implementation implementation
{ TMycInitStateFuncFuture<T> } uses
Myc.Core.Futures, Myc.Core.Tasks;
constructor TMycInitStateFuncFuture<T>.Create(const ATaskFactory: constructor Future<T>.Create(const AFuture: IMycFuture<T>);
IMycTaskFactory; const AInitState: IMycState; AProc: TFunc<T>);
begin begin
inherited Create; FFuture := AFuture;
Assert(Assigned(AInitState), 'AInitState must be assigned.');
FDone := TMycLatch.CreateLatch(1);
// Subscribe the job execution to AInitState.
// The job will run when AInitState notifies the subscriber returned by Run.
// AStartCount for Run is 1, meaning it waits for one Notify from AInitState's subscription.
FInit := AInitState.Subscribe(
ATaskFactory.Run(
procedure
begin
try
try
Self.FResult := AProc();
except
Self.FResult := Default(T); // Set result to Default(T) on error
raise; // Re-raise for TaskFactory to handle
end;
finally
Self.FDone.Notify; // Signal that this future is done (successfully or with error)
end;
end
)
);
end; end;
destructor TMycInitStateFuncFuture<T>.Destroy; class constructor Future<T>.CreateClass;
begin begin
Assert( FInit.State.IsSet ); TMycTaskFactory.AquireTaskManager;
// Explicitly unsubscribe from the initial state to ensure cleanup.
FInit.Unsubscribe;
inherited Destroy;
end; end;
class destructor Future<T>.DestroyClass;
function TMycInitStateFuncFuture<T>.GetDone: IMycState;
begin begin
Result := FDone.State; TMycTaskFactory.ReleaseTaskManager;
end; end;
function TMycInitStateFuncFuture<T>.GetResult: T; function Future<T>.Chain<S>(const Proc: TFunc<T, S>): Future<S>;
begin begin
if not FDone.State.IsSet then var Cap := FFuture;
begin
// Design decision: Caller must ensure future is done.
raise Exception.Create('Result is not yet available for the future.');
end;
// Returns FResult. If AProc failed, FResult is Default(T) and the exception Result := Future<S>.Construct( FFuture.Done,
// was raised to be handled by the TaskFactory. function: S
Result := FResult; begin
Result := Proc( Cap.Result );
end );
end;
class function Future<T>.Construct(const Proc: TFunc<T>): Future<T>;
begin
Result.Create( TMycInitStateFuncFuture<T>.Create( TaskManager, nil, Proc ) );
end;
class function Future<T>.Construct(const Gate: IMycState; const Proc: TFunc<T>): Future<T>;
begin
Result.Create( TMycInitStateFuncFuture<T>.Create( TaskManager, Gate, Proc ) );
end; end;
end. end.
+12 -10
View File
@@ -21,10 +21,11 @@ type
FState: IMycState; FState: IMycState;
FTag: TMycNotifyList<IMycSubscriber>.TTag; // Tag identifying this subscription within the notifier list. FTag: TMycNotifyList<IMycSubscriber>.TTag; // Tag identifying this subscription within the notifier list.
function GetState: IMycState; function GetState: IMycState;
constructor Create(const AState: IMycState; ATag: TMycNotifyList<IMycSubscriber>.TTag);
public public
constructor Create(const AState: IMycState; ATag: TMycNotifyList<IMycSubscriber>.TTag);
// Unsubscribes from the associated signal. // Unsubscribes from the associated signal.
procedure Unsubscribe; procedure Unsubscribe;
class operator Initialize(out Dest: TMycSubscription);
property State: IMycState read GetState; property State: IMycState read GetState;
end; end;
@@ -206,17 +207,20 @@ end;
procedure TMycSubscription.Unsubscribe; procedure TMycSubscription.Unsubscribe;
begin begin
if Assigned(FState) then FState.Unsubscribe(FTag);
begin FState := TMycState.Null;
FState.Unsubscribe(FTag); FTag := nil;
FTag := nil;
FState := nil;
end;
end; end;
function TMycSubscription.GetState: IMycState; function TMycSubscription.GetState: IMycState;
begin begin
Result := FState; Result := FState
end;
class operator TMycSubscription.Initialize(out Dest: TMycSubscription);
begin
Dest.FState := TMycState.Null;
Dest.FTag := nil;
end; end;
{ TMycLatch } { TMycLatch }
@@ -301,7 +305,6 @@ function TMycLatch.Subscribe(const Subscriber: IMycSubscriber): TMycSubscription
var var
alreadySet: Boolean; alreadySet: Boolean;
begin begin
Result.Create(TMycState.Null, nil); // Default to an empty subscription.
if not Assigned(Subscriber) then if not Assigned(Subscriber) then
exit; exit;
@@ -409,7 +412,6 @@ end;
function TMycDirty.Subscribe(const Subscriber: IMycSubscriber): TMycSubscription; function TMycDirty.Subscribe(const Subscriber: IMycSubscriber): TMycSubscription;
begin begin
Result.Create(TMycState.Null, nil); // Default to an empty subscription
if not Assigned(Subscriber) then if not Assigned(Subscriber) then
exit; exit;
+19
View File
@@ -0,0 +1,19 @@
unit Myc.TaskManager;
interface
uses
System.SysUtils,
Myc.Signals;
type
IMycTaskManager = interface
function CreateTask( const Gate: IMycState; const Proc: TProc ): TMycSubscription;
end;
var
TaskManager: IMycTaskManager;
implementation
end.
+3 -1
View File
@@ -24,7 +24,9 @@ uses
TestTasks in 'TestTasks.pas', TestTasks in 'TestTasks.pas',
TestSignals_Dirty in 'TestSignals_Dirty.pas', TestSignals_Dirty in 'TestSignals_Dirty.pas',
Myc.Futures in '..\Src\Myc.Futures.pas', Myc.Futures in '..\Src\Myc.Futures.pas',
TestFutures in 'TestFutures.pas'; TestFutures in 'TestFutures.pas',
Myc.TaskManager in '..\Src\Myc.TaskManager.pas',
Myc.Core.Futures in '..\Src\Myc.Core.Futures.pas';
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit } { keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
{$IFNDEF TESTINSIGHT} {$IFNDEF TESTINSIGHT}
+11
View File
@@ -124,6 +124,8 @@ $(PostBuildEvent)]]></PostBuildEvent>
<DCCReference Include="TestSignals_Dirty.pas"/> <DCCReference Include="TestSignals_Dirty.pas"/>
<DCCReference Include="..\Src\Myc.Futures.pas"/> <DCCReference Include="..\Src\Myc.Futures.pas"/>
<DCCReference Include="TestFutures.pas"/> <DCCReference Include="TestFutures.pas"/>
<DCCReference Include="..\Src\Myc.TaskManager.pas"/>
<DCCReference Include="..\Src\Myc.Core.Futures.pas"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
@@ -1129,6 +1131,15 @@ $(PostBuildEvent)]]></PostBuildEvent>
<Platform value="Win32">True</Platform> <Platform value="Win32">True</Platform>
<Platform value="Win64">True</Platform> <Platform value="Win64">True</Platform>
</Platforms> </Platforms>
<MMX>
<UsesClauseFormatter AutoFormat="0" UseDefault="0">
<GroupNames>Winapi;System.Win;System;Data;REST;Xml;Vcl;FMX</GroupNames>
</UsesClauseFormatter>
<Parser>
<Defines/>
<IfExpressions/>
</Parser>
</MMX>
</BorlandProject> </BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion> <ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions> </ProjectExtensions>
+15 -17
View File
@@ -8,7 +8,7 @@ uses
System.SyncObjs, System.SyncObjs,
Myc.Signals, // For IMycLatch, TMycLatch, IMycState, IMycSubscriber [cite: 62, 68, 53, 45] Myc.Signals, // For IMycLatch, TMycLatch, IMycState, IMycSubscriber [cite: 62, 68, 53, 45]
Myc.Core.Tasks, // For IMycTaskFactory, TMycTaskFactory [cite: 97, 113] Myc.Core.Tasks, // For IMycTaskFactory, TMycTaskFactory [cite: 97, 113]
Myc.Futures; // For IMycFuture, TMycInitStateFuncFuture Myc.Futures, Myc.Core.Futures; // For IMycFuture, TMycInitStateFuncFuture
type type
// Helper class to notify a latch when its Notify method is called. // Helper class to notify a latch when its Notify method is called.
@@ -207,12 +207,8 @@ procedure TTestMycInitStateFuncFuture.Test_GetResult_BeforeDone_RaisesException;
var var
LFuture: IMycFuture<Integer>; LFuture: IMycFuture<Integer>;
LInitLatch: IMycLatch; LInitLatch: IMycLatch;
LExpectedExceptionRaised: Boolean;
const
CExpectedExceptionMessage = 'Result is not yet available for the future.';
begin begin
LExpectedExceptionRaised := False; LInitLatch := TMycLatch.CreateLatch(1);
LInitLatch := TMycLatch.CreateLatch(1); // Create a latch that is not yet set [cite: 77, 212, 330]
LFuture := TMycInitStateFuncFuture<Integer>.Create(FTaskFactory, LInitLatch.State, LFuture := TMycInitStateFuncFuture<Integer>.Create(FTaskFactory, LInitLatch.State,
function: Integer function: Integer
@@ -223,19 +219,21 @@ begin
Assert.IsFalse(LFuture.Done.IsSet, 'Future should not be marked as done initially.'); Assert.IsFalse(LFuture.Done.IsSet, 'Future should not be marked as done initially.');
try Assert.WillRaise(
LFuture.GetResult; procedure
except
on E: Exception do
begin begin
Assert.AreEqual(CExpectedExceptionMessage, E.Message, 'Exception message mismatch.'); LFuture.GetResult;
LExpectedExceptionRaised := True; end );
end;
end;
Assert.IsTrue(LExpectedExceptionRaised, 'Calling GetResult before done should raise an exception.');
LInitLatch.Notify; // [cite: 80, 215, 333] LInitLatch.Notify;
FTaskFactory.WaitFor(LFuture.Done); //
FTaskFactory.WaitFor(LFuture.Done);
Assert.WillNotRaise(
procedure
begin
LFuture.GetResult;
end );
end; end;
procedure TTestMycInitStateFuncFuture.Test_FanIn_OneFutureWaitsForMultipleOthers; procedure TTestMycInitStateFuncFuture.Test_FanIn_OneFutureWaitsForMultipleOthers;