I have the impression from what you write, that a lot of the system is not under your control. Im still a bit uncertain how much you are allowed to change.
I have implemented a global Controller : TSystemController
class which is used to control all the parts.
I understand it is not up to you when a Provider is terminated.
Therefore I believe it is only possible to avoid AV by adding a global ProviderJob List/Queue.
The worker will Push jobs and the provider will Pop jobs.
A Critical Section is used for consistency during Push / Pop by the many involved threads.
I have only used one critical section, since this Queue is responsible for holding all Jobs for the entire system.
A ProviderJob : TProviderJob
, holds a ThreadID to the provider thread to avoid reference to a killed/freed provider. Besides the WorkItem a DoneEvent given by the worker is needed, so the Provider can signal when its done to the waiting worker.
If the Provider is suddenly is killed, the worker will just wait for 30 seconds and time out.
TProviderJob = class
FProviderThreadID : Cardinal;
FWorkItem : TWorkItem;
FWorkDoneEvent : TEvent;
public
property ProviderThreadID : Cardinal read FProviderThreadID;
property WorkItem : TWorkItem read FWorkItem;
property WorkDoneEvent : TEvent read FWorkDoneEvent;
end;
The ProviderJob gets pushed by a Worker thread as follows (no direct reference to the Provider object):
ProviderID := Controller.GetProviderThreadID;
Controller.PushWorkItemToProviderJobList( ProviderID, CurrentWorkItem, WorkDoneEvent );
The worker will wait until the Provider is done with the job:
if WaitForSingleObjehect( WorkDoneEvent.Handle, 30000 ) = WAIT_OBJECT_0 then
begin
if CurrentWorkItem.State = wisProcessedOK then
begin
Inc(Controller.FWorkItemsDone);
Inc(NextWorkItemIdx);
if NextWorkItemIdx >= WorkItems.Count then
Terminate;
end;
end;
The individual Provider is processing jobs as follows:
procedure TProviderThread.Execute;
var
WorkItem: TWorkItem;
WorkDoneEvent: TEvent;
begin
while not Terminated do
begin
Controller.PopNextWorkItemFromProviderJobList( self.ThreadID, WorkItem, WorkDoneEvent );
if (WorkItem<>nil) and (WorkDoneEvent<>nil) then
begin
WorkItem.FState := wisStarted;
Sleep( Round( Random( 5000 )));
WorkItem.FState := wisProcessedOK;
WorkDoneEvent.SetEvent;
end
else
Sleep(500);
end;
end;
Here is an example:
Screenshot of the test app
Here is the full solution:
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 248
ClientWidth = 477
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Memo1: TMemo
Left = 8
Top = 8
Width = 369
Height = 209
Lines.Strings = (
'Memo1')
TabOrder = 0
end
object bStart: TButton
Left = 383
Top = 72
Width = 75
Height = 25
Caption = 'Start'
TabOrder = 1
OnClick = bStartClick
end
object bStop: TButton
Left = 383
Top = 103
Width = 75
Height = 25
Caption = 'Stop'
TabOrder = 2
OnClick = bStopClick
end
object bKillProvider: TButton
Left = 383
Top = 134
Width = 75
Height = 25
Caption = 'Kill Provider'
TabOrder = 3
OnClick = bKillProviderClick
end
object Timer1: TTimer
OnTimer = Timer1Timer
Left = 400
Top = 8
end
end
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Math;
type
TForm1 = class(TForm)
Timer1: TTimer;
Memo1: TMemo;
bStart: TButton;
bStop: TButton;
bKillProvider: TButton;
procedure Timer1Timer(Sender: TObject);
procedure bKillProviderClick(Sender: TObject);
procedure bStopClick(Sender: TObject);
procedure bStartClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
System.SyncObjs,
Generics.Collections;
type
TWorkItemStateE = ( wisNotStarted, wisStarted, wisProcessedOK );
TWorkItem = class
private
FState : TWorkItemStateE;
FID : Integer;
public
property ID : Integer read FID;
property State: TWorkItemStateE read FState;
end;
TWorkItemList = class(TObjectList<TWorkItem>);
TWorkerThread = class(TThread)
protected
procedure Execute; override;
end;
TWorkerThreadList = class(TObjectList<TWorkerThread>);
TProviderThread = class(TThread)
protected
procedure Execute; override;
end;
TProviderThreadList = TObjectList<TProviderThread>;
TProviderJob = class
FProviderThreadID : Cardinal;
FWorkItem : TWorkItem;
FWorkDoneEvent : TEvent;
public
property ProviderThreadID : Cardinal read FProviderThreadID;
property WorkItem : TWorkItem read FWorkItem;
property WorkDoneEvent : TEvent read FWorkDoneEvent;
end;
TProviderJobList = TObjectList<TProviderJob>;
TSystemContoller = class
private
FProviders: TProviderThreadList;
FWorkers : TWorkerThreadList;
FGlobalProviderJobList : TProviderJobList;
FCS_GlobalProviderJobList: TCriticalSection;
FStarted : Boolean;
FRemovedProviders: Integer;
FKilledProviders: Integer;
FRemovedWorkers: Integer;
FWorkItemsDone: Integer;
FStartedDateTime: TDateTime;
procedure ThreadTerminated(Sender: TObject);
procedure AddProvider;
procedure AddWorker;
public
constructor Create;
destructor Destroy; override;
function GetProvider : TProviderThread;
function GetProviderThreadID: Cardinal;
procedure PopNextWorkItemFromProviderJobList(
const AProviderThreadID: Cardinal;
out NextWorkItem: TWorkItem;
out NextWorkDoneEvent: TEvent);
procedure PushWorkItemToProviderJobList(
AProviderThreadID: Cardinal;
AWorkItem: TWorkItem;
AWorkDoneEvent: TEvent);
procedure Start;
procedure Stop;
procedure KillProvider;
function GetStatusReport : String;
end;
var
Controller : TSystemContoller;
{ TWorkThread }
procedure TWorkerThread.Execute;
procedure LoadWorkItems( AWorkItems : TWorkItemList );
var
WorkItemCount: Integer;
n : Integer;
WorkItem: TWorkItem;
begin
// Load work items:
WorkItemCount := 1+Round(Random(10));
for n := 1 to WorkItemCount do
begin
WorkItem := TWorkItem.Create;
WorkItem.FID := n;
AWorkItems.Add(WorkItem);
end;
end;
var
WorkItems : TWorkItemList;
ProviderID: Cardinal;
NextWorkItemIdx: Integer;
CurrentWorkItem: TWorkItem;
WorkDoneEvent : TEvent;
begin
WorkItems := TWorkItemList.Create;
WorkDoneEvent := TEvent.Create(nil, False, False, '' );
try
// load:
LoadWorkItems( WorkItems );
// process work items:
NextWorkItemIdx := 0;
while not Terminated do
begin
CurrentWorkItem := WorkItems[ NextWorkItemIdx ];
ProviderID := Controller.GetProviderThreadID;
Controller.PushWorkItemToProviderJobList( ProviderID, CurrentWorkItem, WorkDoneEvent );
if WaitForSingleObject( WorkDoneEvent.Handle, 30000 ) = WAIT_OBJECT_0 then
begin
if CurrentWorkItem.State = wisProcessedOK then
begin
Inc(Controller.FWorkItemsDone);
Inc(NextWorkItemIdx);
if NextWorkItemIdx >= WorkItems.Count then
Terminate;
end;
end;
Sleep(1000);
end;
finally
WorkDoneEvent.Free;
WorkItems.Free;
end;
end;
{ TProviderThread }
procedure TProviderThread.Execute;
var
WorkItem: TWorkItem;
WorkDoneEvent: TEvent;
begin
while not Terminated do
begin
Controller.PopNextWorkItemFromProviderJobList( self.ThreadID, WorkItem, WorkDoneEvent );
if (WorkItem<>nil) and (WorkDoneEvent<>nil) then
begin
WorkItem.FState := wisStarted;
Sleep( Round( Random( 5000 )));
WorkItem.FState := wisProcessedOK;
WorkDoneEvent.SetEvent;
end
else
Sleep(500);
end;
end;
{ TSystemContoller }
constructor TSystemContoller.Create;
begin
inherited;
FStartedDateTime:= now;
FCS_GlobalProviderJobList := TCriticalSection.Create;
FGlobalProviderJobList := TProviderJobList.Create;
FProviders:= TProviderThreadList.Create;
FProviders.OwnsObjects := False;
FWorkers := TWorkerThreadList.Create;
FWorkers.OwnsObjects := False;
end;
destructor TSystemContoller.Destroy;
begin
FCS_GlobalProviderJobList.Free;
FGlobalProviderJobList.Free;
FWorkers.Free;
FProviders.Free;
inherited;
end;
procedure TSystemContoller.Start;
var
n: Integer;
begin
if not FStarted then
begin
FStarted := True;
for n := 1 to 5 do
AddProvider;
for n := 1 to 10 do
AddWorker;
end;
end;
procedure TSystemContoller.Stop;
var
n: Integer;
begin
for n := FProviders.Count-1 to 0 do
FProviders[n].Terminate;
for n := FWorkers.Count-1 to 0 do
FWorkers[n].Terminate;
FStarted := False;
end;
procedure TSystemContoller.KillProvider;
var
Provider: TProviderThread;
begin
Provider := GetProvider;
if Provider<>nil then
begin
if not Provider.Terminated then
begin
GetProvider.Terminate;
Inc( FKilledProviders );
end;
end;
end;
procedure TSystemContoller.AddProvider;
var
Provider: TProviderThread;
begin
Provider := TProviderThread.Create(True);
Provider.OnTerminate := ThreadTerminated;
Provider.FreeOnTerminate := True;
FProviders.Add( Provider );
Provider.Start;
end;
procedure TSystemContoller.AddWorker;
var
Worker: TWorkerThread;
begin
Worker := TWorkerThread.Create(True);
Worker.OnTerminate := ThreadTerminated;
Worker.FreeOnTerminate := True;
FWorkers.Add( Worker );
Worker.Start;
end;
procedure TSystemContoller.ThreadTerminated(Sender : TObject );
begin
if Sender is TProviderThread then
begin
FProviders.Remove(TProviderThread(Sender));
Inc(FRemovedProviders);
if FStarted then
AddProvider;
end
else
if Sender is TWorkerThread then
begin
FWorkers.Remove(TWorkerThread(Sender));
Inc(FRemovedWorkers);
if FStarted then
AddWorker;
end;
end;
procedure TSystemContoller.PushWorkItemToProviderJobList(
AProviderThreadID: Cardinal;
AWorkItem: TWorkItem;
AWorkDoneEvent: TEvent);
var
ProviderJob: TProviderJob;
begin
FCS_GlobalProviderJobList.Enter;
try
ProviderJob := TProviderJob.Create;
ProviderJob.FProviderThreadID := AProviderThreadID;
ProviderJob.FWorkItem := AWorkItem;
ProviderJob.FWorkDoneEvent := AWorkDoneEvent;
FGlobalProviderJobList.Add( ProviderJob );
finally
FCS_GlobalProviderJobList.Leave;
end;
end;
procedure TSystemContoller.PopNextWorkItemFromProviderJobList(
const AProviderThreadID: Cardinal;
out NextWorkItem: TWorkItem;
out NextWorkDoneEvent: TEvent);
var
n : Integer;
begin
FCS_GlobalProviderJobList.Enter;
try
NextWorkItem := nil;
NextWorkDoneEvent := nil;
for n := 0 to FGlobalProviderJobList.Count-1 do
begin
if FGlobalProviderJobList[n].ProviderThreadID = AProviderThreadID then
begin
NextWorkItem := FGlobalProviderJobList[n].WorkItem;
NextWorkDoneEvent := FGlobalProviderJobList[n].WorkDoneEvent;
FGlobalProviderJobList.Delete(n);
Exit;
end;
end;
finally
FCS_GlobalProviderJobList.Leave;
end;
end;
function TSystemContoller.GetProvider: TProviderThread;
var
ProviderIdx: Integer;
begin
ProviderIdx := Trunc(Random( FProviders.Count ));
if InRange(ProviderIdx, 0, FProviders.Count-1 ) then
Result := FProviders[ ProviderIdx ]
else
Result := nil;
end;
function TSystemContoller.GetProviderThreadID: Cardinal;
var
Provider: TProviderThread;
begin
Provider := GetProvider;
if Provider<>nil then
Result := Provider.ThreadID;
end;
function TSystemContoller.GetStatusReport: String;
const
cState : array[Boolean] of string = ( 'Stopped', 'Started' );
begin
Result := 'Start Date and Time: ' + DateTimeToStr(FStartedDateTime) + #13#10+
'Date and Time: ' + DateTimeToStr(now) + #13#10+
'System State: ' + cState[FStarted] + #13#10+ #13#10 +
'Queued Work Items: ' + IntToStr( self.FGlobalProviderJobList.Count )+ #13#10 +
'Work Items Done: ' + IntToStr(FWorkItemsDone)+ #13#10 + #13#10 +
'Current Providers: ' + IntToStr( self.FProviders.Count ) + #13#10+
'Removed Providers: ' + IntToStr( FRemovedProviders ) + #13#10 +
'Random Provider Kills: ' + IntToStr(FKilledProviders)+ #13#10 + #13#10 +
'Current Workers: ' + IntToStr( self.FWorkers.Count ) + #13#10 +
'Removed Workers: ' + IntToStr( FRemovedWorkers );
end;
procedure TForm1.bKillProviderClick(Sender: TObject);
begin
Controller.KillProvider;
end;
procedure TForm1.bStartClick(Sender: TObject);
begin
Controller.Start;
end;
procedure TForm1.bStopClick(Sender: TObject);
begin
Controller.Stop;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Memo1.Text := Controller.GetStatusReport;
if Random(100) < 30 then
Controller.KillProvider;
end;
initialization
Controller := TSystemContoller.Create;
Controller.Start;
finalization
Controller.Stop;
Controller.Free;
end.