-3

I have several threads (Providers), that are used by other threads (Workers) on concurrent basis.

Several threads means severan critical sections according to threads.

Is it impossble to place a critical section variable inside a thread class, or its better to hold separate array for critical section of each thread that needs to be concurrently accessed?

Or there is a better practice?

UPD: a bit explanation.

Process flow is:

  1. Worker loads a pool of data to its self
  2. In cycle of own pool of data a Worker try to gain access to a Provider
  3. Worker gains access to Provider ->
  4. Worker locks it ->
  5. Worker posts data to Provider from own pool ->
  6. Provider start doing a job ->
  7. When job is done Provider sends event to Worker ->
  8. Worker gets a result ->
  9. Worker release lock ->
  10. Goto 2 or exit if last data from pool was processed

This actually works fine in a real situation with a very good outcome. But the problem is when I need to kill one of the Providers.

If a critical section inside a Provider class, then its possible to get access violation once a Worker already entered CS and waiting for unlock and a provider is not really terminated yet, but terminated next step.

if not Provider.Terminated then
   Provider.CS.Enter;  //This is a place where AV occurs.
Provider.PostData(Worker.Data);
Provider.StartJob;
WaitForSingleObject(Provider.EventJobIsDone, 30000);
Provider.CS.Leave; //or here 

If I have to place CS outside - how it is better to do this and to test that a provider is terminated?

Several Providers and many Workers, that are created / terminated at any time and should work together. There is no option, that one Worker process all its pool of data while others are waiting. Each Worker do one iteration and then waits while other Workers do the same, then again it do next iteration then again waits while others do the same and so on until all pools are proccessed and Workers are terminated (each on its own time, when pool is finished).

Each iteration of Worker and result it gets influence what Provider would be chosen for that Worker next time.

And again - this works fine in real conditions and no stucks happen. Very fast and quite as it is expected to work.

Example

A rough example.

You have Worker threads, that load a pool of data from DB. There are different DBs, not only one. Each item - is a heavy picture 0.1Mb-10Mb - you never know before. Imagine each Worker has 100-5000 items to be processed - you need to convert it to a lesser picture or do some other process. You never know how many items there would be in any next Worker thread. It could be 10, 1000, 5000, more or less. Whenever a Worker thread is started - there should be no timeout for a new Worker thread to start processing its first item from data pool, because it's a waste of time that costs.

You have 10 Providers, that are connected to 10 machines on the local network. Those machines do all the work you need remotely. (a rough example, please)

You should balance (the real strategy is more complex and depends on the actual data - it's similar to: is it a JPG, TIFF, PNG, BMP or whatever format, but simply please think of balance) traffic among all the Providers correctly. To do this you should count the quantity of processed data and decide each iteration where to put next Worker call - to Provider1,2,3...or 10. Besides you need to return a result immediately to the calling Worker, because a Worker should give an immediate report, write a flag to DB (only Worker knows which DB) that data is processed (!important) and so on.

That's are the conditions. In brief:

  • 10-50 Workers at a time with 100-5000 items
  • Workers are started and finished on their own
  • You can't do a prognosis how many Workers there would be at any time
  • Service is working 24/7 and ready to process data
  • 10 Providers, each is a connection to a machine on the network
  • A new Worker should as soon as possible start processing its data
  • A need to balance traffic among Providers on-the-fly
  • Amount of processed data thru each Provider affects the decision to choose next Provider for a next data process.
  • Worker should always get a result and update data to DB
  • Suddenly you are about to terminate a Provider
  • Some Workers already waiting to process data with this one Provider

How to tell the Workers to stop waiting and when they are unfreezed - how to avoid Access violation that is pretty sure may occur on the next step in Worker flow (which is presented in the code in above).

notricky
  • 179
  • 1
  • 2
  • 18
  • 2
    Execute needs to be overriden. Your code hides. Can't we have a [mcve]? – David Heffernan Oct 11 '15 at 17:48
  • @DavidHeffernan I've just added those directives - for sure they are to be there and for sure they are. I was writing by hand because there is a lot of code in my project, lots of inherits, base clasess and properties that are there (i.e. FOnExecute, FOnTerminate and etc.) and finally to make it simple and readable here I have to rewrite it again. Main things are mentioned in the code and have same relevance to real code. Or you would like to see more code? – notricky Oct 11 '15 at 17:56
  • Why does TWorkThread.Execute do "while PT = Nil do ... Sleep() ..." instead of waiting on an event? – MartynA Oct 11 '15 at 17:57
  • 5
    Don't post fake code. It's downright rude to ask for help and then tell us that you didn't even give us the code you are using. Don't send us on a wild goose chase as if you think our time is irrelevant. Make a [mcve] please. – David Heffernan Oct 11 '15 at 17:59
  • @MartynA well. Haven't thought that way - this solution fits me. But I guess it's possible to add to each TWorkThread additional Event that would be signaled each time it gets a Provider. Inside `GetProvider` there is a pool that is incremented on each try of TWorkThread to get a certain provider and cleared, when count is equal to count of TWorkThreads. But would this change a main problem? – notricky Oct 11 '15 at 18:03
  • I realise you said this is a simplified version but your WorkThreads and ProviderThread look like they have an unnecessarily incestuous relationship. Maybe isolate them a bit more and communicate more using PostThreadMessage? If you use a message-based Execute loop, it provides a graceful way for a thread to finish up and terminate itself. – MartynA Oct 11 '15 at 18:07
  • @MartynA no there is no other functionality in "complicated" version. In real app each ProviderThread holds a connection to an outer service, while WorkThreads can process its data through one of the Providers. WorkThreads can be created at any time and must work in parralel doing their job one-by-one sequentally. 30-40 WorkThreads for 5-6 Providers. And ofcourse there are rules by which this or that provider is chosen for current WorkThread. But it doesn't do any weather, because in my test app the only rule is RandomRange and no real connections are held. But the problem still happens. – notricky Oct 11 '15 at 18:16
  • @DavidHeffernan this is not fake code. But it illustrates the situation clearly (except "override" verbs). Or its not? I was trying to make simple version just to make it more clear for benefits of all readers. Look by yourself. Base class has: procedures OnTerminate, Execute, Initial dynamic; (where everything is initialized including OnExecute, AfterConstruction and etc) AfterConstruction, IncRef, DecRef properties: OnExecute, OnConstruction, Ondestruction, OnIncRef, OnDecRef. This is really lots of everything and complicated. I've just made it simple. But it will produce minimal work still – notricky Oct 11 '15 at 18:26
  • 2
    Yes it is fake code. It's not code that executes. You said so yourself. Please don't make this hard for yourself. Make a [mcve]. Then it will be simple for us to tell you what you did wrong. – David Heffernan Oct 11 '15 at 18:50
  • @DavidHeffernan Ok. I'm already working on this. – notricky Oct 11 '15 at 19:00
  • I have changed the code. Now it is ready to copy and paste to test. Have fun ;) If this is important - I am using Delphi XE7. By clicking "check" I can find out if one of the WorkThreads is stucked by seeing the same and unchanged number in the memo. I thought the problem could be solved, but nope - its still there ;( Hope this finally helps. – notricky Oct 11 '15 at 20:00
  • Please note, that I didn't handle all errors and possible AV's in this test project, because its not important here. For example it is a good idea when clicking "check" button to read `TWorkThreads` status to lock TWorkThreads from being destroyed and etc. Main reason is to find that "yes" - there is a problem when terminating `TProviderThread`, when some WorkThreads become stucked. – notricky Oct 11 '15 at 20:55
  • That's strange. But if comment out `CrtWork.Enter` on termination check in `TProviderThread.Execute` - works fine. Only I got - sometimes AVs (which btw I don't know how to handle - I got AV when WorkThread tries to `PT.CrtWork.Enter;`), but nothing got stucked. What's the point? – notricky Oct 11 '15 at 21:36
  • This code is simply beyond redemption. It cannot be saved. What problem are you trying to solve? – David Heffernan Oct 11 '15 at 22:17
  • @DavidHeffernan I didn;t understand your first and second sentences. As for the question: The problem was that WorkThreads when a ProvierThread is destroyed became stucked. It seems that it was a bad idea to enter critical section on termination of the provider. I guess now the only problem left - is access violations when Provider already terminated, but WorkThread tries to Enter to its critical section... – notricky Oct 11 '15 at 22:29
  • 2
    I mean that there are so many problems with the code at all levels that your best course of action is to scrap it and start again. But start again with a clear statement of the problem. There will for sure be existing robust solutions that you can use. – David Heffernan Oct 12 '15 at 06:24
  • @DavidHeffernan clear statement of the problem is already there: how to destroy a thread safely, while other threads are concurrently try to use that thread. – notricky Oct 13 '15 at 17:25
  • As I said, I think that this code cannot be salvaged – David Heffernan Oct 13 '15 at 17:48
  • Then why did you ask me to write it? @DavidHeffernan – notricky Oct 13 '15 at 17:50
  • So we could see it. Now we can see it, we can critique it. You still didn't say what the code is trying to achieve. – David Heffernan Oct 13 '15 at 18:09
  • @DavidHeffernan I have changed the question because I cannot post any more question on this topic or close to it. I need not a code but an understanding what is the better way - syncronization object inside a class or outside. And why. – notricky Oct 13 '15 at 18:25
  • Your best bet is to use a threading library. If you do that you can have code that works immediately. I'd start by looking into OTL, and reading all the excellent documentation and examples that are provided. – David Heffernan Oct 13 '15 at 18:29
  • @DavidHeffernan yes, that might be, but I just want to point out it in common libriaries. It's not only me to decide what shoud one use. That's why I'm asking about a common way implementing threads rather than using PlugAndPlay libraries. – notricky Oct 13 '15 at 18:35
  • I'm not sure what you mean. You want several worker threads access common data (protected by a critical section) in one provider thread? That should be fine as long as one worker thread does not forget to release the CS once acquired. – LU RD Oct 13 '15 at 18:47
  • @LURD worker thread is posting data to provider thread. And provider thread is doing a job and then reports back the result. Worker thread reads the result on event and makes some other stuff and releases the CS for other worker threads to gain access. Actually a worker thread - is a interface given to other developers. A bridge between us - 1) data that is posted from W to P thread 2) result that is obtained back. So this is a bit more complicated, but still can be viewed as an access to protected data. The problem is - where to hold a CS - inside Provider class or have a global array of CSs – notricky Oct 13 '15 at 19:00
  • Who controls the life time of the worker threads? As long as the provider threads outlives all worker threads, I see no problem. If not, you need global (in scope) data structures or a way to signal the worker threads which can be very messy. Usually I queue a job (threadsafe queue) together with a callback method to the provider. The provider takes the job from the queue and processes the data and when done, uses the callback method to pass the result. No CS needed and everything is serialized inside the provider. – LU RD Oct 13 '15 at 19:21
  • @LURD I have edited the question so maybe if you look you will get a better understanding. Please. About your question: the life time of worker threads are controlled by outside (not me): it is finished by either itself when a data pool is over or by a command to terminate. Because there are several providers and there is some way they are chosen (depending on each iteration of each worker thread and results they got). I dont see a way to use a callback. Well, it is possible, but this should be done on each iteration. In any way all worker threads should work together. One-iteration-by-one. – notricky Oct 13 '15 at 19:29
  • Just let the worker threads wait for the last result before passing the next job to the queue. And make sure that the worker thread is not terminated with a pending request. And if a provider can be removed at any time, use a global queue. – LU RD Oct 13 '15 at 19:37
  • @LURD yes this is done. What is not done - I have showed in a code. When I terminate a ProviderX - other Workers might already gain access to ProviderX thru its CS. When ProviderX finished a job it gets `Terminated = true` and finishes execution. While previous Worker releases CS - I get AV, because CS is freed. Well because all based on events - there is an opportunity to get AV. So in OnTerminate proc I should be absolutely sure that no one Worker is about to enter or leave CS – notricky Oct 13 '15 at 20:02
  • Am I right? The solution is to count references for each Provider, yes? Maybe there is a way to understand how many Workers are waiting (by entering a CS)? – notricky Oct 13 '15 at 20:07
  • What you are trying to do is not easy. One must never access a thread that could be terminated without notice.There are better ways, with a global queue as I mentioned. Everyting will be serialized, workers and providers can live or die independently provided they obey simple rules. – LU RD Oct 13 '15 at 20:34
  • Again I have updated the question and added an **Example** at the bottom. An example is quite rough with other issues to discuss, but pretty fine illustrates a certain real situation. – notricky Oct 13 '15 at 23:54

2 Answers2

0

Your problem boils down to following scenario:

  • A provider thread with a CS inside has to terminate.
  • Multiple worker threads could be blocked, waiting for the CS to be released.
  • Provider thread is terminated just after the CS is released, causing all pending threads to access a non-existing thread.

This is a design problem, never access a thread that could be terminated without notice. You could solve it by some communication. Perhaps let the provider live long enough to tell all other worker threads that it is time to say goodbye. Set a semaphore, answer all pending requests with an end of life message. Make sure the worker threads looks at the semaphore before sending a request.

LU RD
  • 34,438
  • 5
  • 88
  • 296
  • Yes, something like that. But what it should be? Another flag or reference counting? It's not still clear to me. – notricky Oct 13 '15 at 23:05
  • I have added an example in the question to illustrate a situation. An idea with a global queue is good, but still sync problems left. Please look at the example and see that sync is still would be needed in case of global queue. By that is possible to avoid problems with termination of Providers, but the point is that data is posted to queue one by one. Never all the data is transferred to a global queue, because it means, that Worker should wait until previous Worker finished. That would bring us a problem with throttling of separated data process. – notricky Oct 13 '15 at 23:24
  • Your question has become too broad to fit into the SO guidelines. The reason why the provider termination gives an AV is well analysed and understood. You have the bits and pieces to go on and solve the bigger scope. Make a simple model of the interaction between all parts, and once it works, scale it up. As I mentioned in comments, a thread-safe queue can make it easier to decouple dependencies between the workers and providers. – LU RD Oct 14 '15 at 06:20
  • Well, maybe it is. I was just saying that a change of task is not permitted. The task is as is. I really don't understand how a thread-safe queue might solve the problem. I mean - I need to solve a problem whether current Worker thread should put its next item to a queue or not. And this would depend on previous Workers job whitch was done. And also at any time a new Worker might be created. And it should start immediately. It looks like a plenty of items from all Workers are put at the same time to queue and wait until last item is deleted from queue and cycle starts again... – notricky Oct 14 '15 at 13:24
  • Well, the queue has the advantage over a CS in this case. They perform more or less the same task, but workers will be decoupled from providers and the termination of a provider will be easier to handle. Workers still have to wait for an answer before puttting a new item on the queue, no change here (just much easier code though). You have to manage the situation if a provider goes down with items still on the queue, but it seems there are some logic supervising the flow. – LU RD Oct 14 '15 at 13:39
  • I think in any case a CS should be. At least on posting, updating or deleting from queue. And yes, there is a Controller, which counts the quantity of Workers and says them which Provider to use. I think it's an idea. But yet there is a problem. What is the way to sleep each Worker and how to wake them up? Events? CS? If Events - WaitForSingleObjects' timeout would be undefined... which is bad. And I still don't see how to force several Providers do run jobs at a time? – notricky Oct 14 '15 at 14:08
0

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.