proper construction of ParallelTask (IOmniParallelTask) with cancellation & termination handler - delphi

I am just playing around with the OmniThreadLibrary after reading the docs, but I am still facing some simple/early problems on constructing a ParallelTask.
After the construction of a ParallelTask with cancellationToken and terminationHandler, terminationHandler.OnTerminated and OnStop are not being executed after the async execution is done and I was not able to find out why :-(
I hope some of the OTL pros can help me out on this one.
What I want to achieve:
execute an asynchronous operation
stop execution when cancelled (cancellationtoken)
execute some code in the mainthread (checking for exceptions) when the async operation is done
What I did so far:
After reading the docs I created a ParallelTask, setting up cancellationToken and terminationHandler via TaskConfig and executed the operation.
The executed operation itself checks for the cancellationToken being signalled and does its work (here a Sleep of 1s). The HandleOnTerminated method checks for
errors and sets the fIsDone and fHasError flags, getting read by someone from mainthread.
unit OTLSetup.Async;
interface
uses
OtlParallel, OtlSync, OtlTaskControl, OtlTask;
type
IAsyncOperation = interface
['{6B10AB46-DEB6-48F5-AC36-E9327AA54C82}']
procedure Execute;
procedure Cancel;
function IsDone: boolean;
end;
TAsyncOperation = class(TInterfacedObject, IAsyncOperation)
protected
fParallelTask: IOmniParallelTask;
fCancellationToken: IOmniCancellationToken;
fIsDone: boolean;
procedure HandleOnTerminated(const task: IOmniTaskControl);
procedure HandleOnStop;
procedure AsyncOperation(const task: IOmniTask);
public
procedure Execute;
procedure Cancel;
function IsDone: boolean;
end;
implementation
uses
Winapi.Windows;
{ TAsyncOperation }
procedure TAsyncOperation.Cancel;
begin
fCancellationToken.Signal;
end;
procedure TAsyncOperation.Execute;
begin
if Assigned(fParallelTask) then
Exit;
fIsDone := false;
fCancellationToken := CreateOmniCancellationToken;
fParallelTask := Parallel.ParallelTask;
fParallelTask.NoWait.NumTasks(1);
fParallelTask.TaskConfig(Parallel.TaskConfig.CancelWith(fCancellationToken).OnTerminated(HandleOnTerminated));
fParallelTask.OnStop(HandleOnStop);
fParallelTask.Execute(AsyncOperation);
end;
procedure TAsyncOperation.AsyncOperation(const task: IOmniTask);
var
I: Integer;
begin
for I := 0 to 5 do
if task.CancellationToken.IsSignalled then
Exit
else
Winapi.Windows.Sleep(1000);
end;
procedure TAsyncOperation.HandleOnStop;
begin
fParallelTask := nil;
fIsDone := true;
end;
procedure TAsyncOperation.HandleOnTerminated(const task: IOmniTaskControl);
begin
fParallelTask := NIL;
fIsDone := true;
end;
function TAsyncOperation.IsDone: boolean;
begin
result := fIsDone;
end;
end.
With this peace of Code, fIsDone is never set, because HandleOnTerminate and HandleOnStopare never called. So with the exmaple from above
the following ConsoleApplication seems to never end:
program OTLSetup;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
OTLSetup.Async in 'OTLSetup.Async.pas';
var
LAsync: IAsyncOperation;
begin
LAsync := TAsyncOperation.Create;
try
LAsync.Execute;
while not LAsync.IsDone do
Writeln('Async task still running');
Writeln('Async task finished');
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.

As posted in the comments, the issue I was facing has been caused by the consoleapplication itself, because it does not contain a messageloop (in my case a DUnitX project).
Because the OTL communication seems to be based on windowsmessages, OnTerminated and OnStop are not triggered in absence of a working messageloop.

Related

Delphi OmniThreadLibrary and Async

Hopefully a simple one. I am using an OTL Parallel.For loop to process lots of data. The amount of data can change and if there is a lot (that takes over 2 seconds) Windows flickers the application form and gives a temporary "not responding" status in the title bar.
To get around this I thought I could put the procedure with the Parallel.For loop inside an OTL Async call, like
done:=false;
Async(ProcedureThatDoesParallelFor).Await(
procedure begin
done:=true;
end);
repeat application.processmessages until done=true;
This works (or seems to work) but can lead to the program just aborting/exiting without any error messages. It only seems to cause the silent abort problem when the Parallel.For loop is very quick to run.
If I remark the above code and take the call to ProcedureThatDoesParallelFor outside of it the app runs fine without unexpected quitting, so I am assuming it must be the Async call causing the problem. Or a combination of Parallel.For within Async?
Is using Async the best way to run another procedure and wait for it to finish? Is there a better OTL way of doing this?
Thanks for any ideas or solutions.
Here is the simplest example to show the crashing error. Single form with a memo and button. Click the button and the program will hang around iteration 300.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,OtlParallel,OtlTaskControl;
type
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure AsyncParallelFor;
var iterations:integer;
blah:integer;
begin
iterations:=10;
//for iter:=0 to limit-1 do
Parallel.For(0,iterations-1).Execute(procedure(iter:integer)
var x,y:integer;
begin
for y:=0 to 50 do
begin
for x:=0 to 50 do
begin
blah:=x+y;
end;
end;
end);
end;
procedure AsyncProcedure;
var done:boolean;
begin
done:=false;
Parallel.Async(
procedure
begin
//executed in background thread
AsyncParallelFor;
end,
Parallel.TaskConfig.OnTerminated(
procedure (const task: IOmniTaskControl)
begin
//executed in main thread after the async has finished
done:=true;
end
)
);
//this point is reached immediately after the call to Async
//the repeat loop waits until the Async is finished being signalled via done variable
repeat
application.processmessages;
until done=true;
end;
procedure TForm1.Button1Click(Sender: TObject);
var iters:integer;
begin
iters:=0;
repeat
memo1.lines.add('Iteration '+inttostr(iters)+'...');
memo1.lines.add('Before Async');
application.processmessages;
AsyncProcedure;
memo1.lines.add('After Async');
application.processmessages;
inc(iters);
until 1>2;
end;
end.
AsyncParallelFor shows the basic nested loops. Just a simple addition in there to demo the issue.
AsyncProcedure does the OTL Async call and waits for the return.
I have a lot of non parallel code before and after the call to AsyncProcedure that need to wait for the parallel.for loop to finish.
If I change the button click to call AsynParallelFor directly without the Async then there is no hang.
In your AsyncProcedure, there is no need to repeatedly wait for the async call to finish. This defeats the event driven model that the OS is built on. Specially calling Application.ProcessMessages can lead to unexpected things to happen.
Use the OnTerminate event to signal that the async call is done and there take actions what to do next. In the example provided in this answer, a callback method is used to handle that.
A button click method is supposed to do only a short task, not an eternal loop with the dreaded calls to Application.ProcessMessages.
Instead, use a flag to indicate whether a new call to the async procedure should be done.
Below is an example how to modify your test with a callback method and an event driven model (I did not try the OTL calls, but I would be surprised if the library is the cause of your problems):
type
TForm1 = class(TForm)
BtnStart: TButton;
BtnStop: TButton;
Memo1: TMemo;
procedure BtnStartClick(Sender: TObject);
procedure BtnStopClick(Sender: TObject);
private
{ Private declarations }
fDoRepeat : Boolean;
fIterations : Integer;
procedure MyCallbackMethod(Sender : TObject);
public
{ Public declarations }
end;
procedure AsyncProcedure( MyCallbackMethod : TNotifyEvent);
begin
Parallel.Async(
procedure
begin
//executed in background thread
AsyncParallelFor;
end,
Parallel.TaskConfig.OnTerminated(
procedure (const task: IOmniTaskControl)
begin
//executed in main thread after the async has finished
MyCallbackMethod(Nil);
end)
);
end;
procedure TForm1.MyCallbackMethod(Sender : TObject);
begin
if (Sender = nil) then // Callback from AsyncProcedure
memo1.lines.add('After Async');
if fDoRepeat then begin
Inc(fIterations);
memo1.lines.add('Iteration '+inttostr(fIterations)+'...');
memo1.lines.add('Before Async');
AsyncProcedure(MyCallbackMethod);
end;
end;
procedure TForm1.BtnStartClick(Sender: TObject);
begin
fDoRepeat := true;
fIterations := 0;
BtnStart.Enabled := false;
MyCallbackMethod(Sender); // Start iteration event looping
end;
procedure TForm1.BtnStopClick(Sender: TObject);
begin
fDoRepeat := false; // Stop iteration loop
BtnStart.Enabled := true;
end;
Update
Running the above test in debug mode gave:
Out of memory
after 387 iterations in an OTL unit allocating memory for a buffer (and it is running slow).
Testing the OTL Parallel.For() with some other examples from Updating a Progress Bar From a Parallel For Loop (Plus Two Bonuses) did not improve the outcome. Program hangs at 400 iterations.
Using the bug ridden Delphi PPL did in fact work, though.
Uses
Threading;
procedure AsyncParallelFor;
var
iterations:integer;
blah:integer;
begin
iterations := 10;
TParallel.For(0,iterations-1,
procedure(iter : integer)
var x,y:integer;
begin
for y := 0 to 50 do
begin
for x := 0 to 50 do
begin
blah := x+y;
end;
end;
end);
end;
procedure AsyncProcedure( MyCallbackMethod : TNotifyEvent);
begin
TTask.Run(
procedure
begin
AsyncParallelFor;
//executed in main thread after the async has finished
TThread.Queue(nil,
procedure
begin
MyCallbackMethod(Nil);
end
);
end);
end;
To update the GUI within a parallel for loop, just use this code within the loop:
TThread.Queue(nil,
procedure
begin
// Some code that updates the GUI or calls a method to do so.
end
);

Delphi - How can I ask for user input while other segments of the code are running

The game i'm trying to make is snake, in the console application. I can get the snake to move along the screen however I am not sure how I can read the user inputing the keys WASD, code segment shown below.
write (StoredTrail); //This would be writing the snake, each segment is '[]'
repeat
clearScreen; // This is calling a clear screen procedure, if there is a simple way to make the snake disappear from the console that avoids such a lengthy procedure that would be great to know.
delete (StoredTrail ,0,2);
StoredTrail:= A+StoredTrail; //This makes the trail move along(A is ' ')
write(StoredTrail);
Xcord:= Xcord + 1;
sleep(150);
until 1=2;
I am also aware the sleep is very inefficient so if anyone had a better way to delay the movement of the snake that would also be welcomed. Coding for increasing the snakes length is also not implemented yet.
Many thanks to anyone able to help.
I give an example for a event driven console application, which update the screen iterativelly.
It would be too long to write here the user event handler routines and you can find it on a lot of places on the net. This is a fine example, which handle keyboard and mouse events as well:
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils
, Vcl.ExtCtrls
;
type
TSnakeApp = class
private
fTimer : TTimer;
fExit : boolean;
protected
function createTimer : TTimer; virtual;
procedure releaseTimer; virtual;
procedure drawSnake( timer_ : TObject ); virtual;
procedure handleKeyBoardEvents; virtual;
public
constructor create;
destructor destroy; override;
procedure run;
end;
var
app : TSnakeApp;
function TSnakeApp.createTimer : TTimer;
begin
result := TTimer.Create( NIL );
end;
procedure TSnakeApp.releaseTimer;
begin
fTimer.Free;
end;
procedure TSnakeApp.drawSnake( timer_ : TObject );
begin
// if it takes too long time (>= times.interval), then disable+enable the timer
fTimer.enabled := FALSE;
try
finally
fTimer.enabled := TRUE;
end;
end;
procedure TSnakeApp.handleKeyBoardEvents;
begin
// It would be too long to write here, but you can find a very nice keyboard/mouse event handler for console applications here:
// https://learn.microsoft.com/en-us/windows/console/reading-input-buffer-events
// case ( keyPressed ) of
// VK_ESC:
// fExit := TRUE;
// ...
end;
constructor TSnakeApp.create;
begin
inherited create;
fTimer := createTimer;
fTimer.Interval := 20;
fTimer.OnTimer := drawSnake;
end;
destructor TSnakeApp.destroy;
begin
releaseTimer;
inherited destroy;
end;
procedure TSnakeApp.run;
begin
fTimer.enabled := TRUE;
while ( not fExit ) do
begin
handleKeyBoardEvents;
end;
fTimer.enabled := FALSE;
end;
begin
try
try
app := TSnakeApp.create;
app.run;
finally
app.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
In the days of Turbo Pascal an ancient predecessor of Delphi there was a CRT unit that provided some useful functions for console applications. Two such functions that would be of interest to you for keyboard input are KeyPressed() which returns true if a key has been pressed and GetKey() which returns the key pressed. For Delphi itself there are a few sources of libraries that offer compatible functions. One is Rudy's Velthuis.Console unit.

Looping without causing app to freeze

I would like to write a loop that checks the value of a variable has changed. There's no event that fires to tell me the value has changed.
The application doesn't support multi threading.
How to achieve this without causing app to freeze ?
The aim is this:
Application starts
...
loop
Check variable value
If changed then
exit
if timedOut then
exit
While loop causes application to freeze.
Thank you.
* Edit *
This is what I'm after (this code is written by Remy Lebeau):
const
APPWM_COM_EVENT_DONE = WM_APP + 1;
APPWM_COM_EVENT_TIMEOUT = WM_APP + 2;
type
MyClass = class
private
MsgWnd: HWND;
procedure COMEventHandler(parameters);
procedure WndProc(var Message: TMessage);
public
constructor Create;
destructor Destroy; override;
procedure DoIt;
end;
constructor MyClass.Create;
begin
inherited;
MsgWnd := AllocateHWnd(WndProc);
end
destructor MyClass.Destroy;
begin
KillTimer(MsgWnd, 1);
DeallocateHWnd(MsgWnd);
inherited;
end;
procedure MyClass.COMEventHandler(parameters);
begin
KillTimer(MsgWnd, 1);
PostMessage(MsgWnd, APPWM_COM_EVENT_DONE, 0, 0);
end;
procedure MyTimer(hWnd: HWND; uMsg: UINT; idEvent: UINT_PTR; dwTime: DWORD); stdcall;
begin
KillTimer(hWnd, idEvent);
PostMessage(hWnd, APPWM_COM_EVENT_TIMEOUT, 0, 0);
end;
procedure MyClass.WndProc(var Message: TMessage);
begin
case Message.Msg of
APPWM_COM_EVENT_DONE:
begin
// Event fired, all good
end;
APPWM_COM_EVENT_TIMEOUT:
begin
// Event timed out
end;
else
begin
Message.Result := DefWindowProc(MsgWnd, Message.Msg, Message.WParam, Message.LParam);
end;
end;
end;
procedure MyClass.DoIt;
begin
SetTimer(MsgWnd, 1, 1000 * 1000, #MyTimer);
// invoke COM function that will eventually trigger the COM event...
end;
How to call DoIt and wait for either Event to fire or timeout without causing the application to freeze ?
Tried using while do loop but that prevented WndProc from running.
Thank you
Answer depends on your application demands. There are 2 easy solutions with prons and cons each:
1. Put Timer to application and check value by timeout. Dignity - it is the most easy way for GUI application (Windows messages loop already exists), drawback on other side - there will be delta time of detecting value have been changed.
2. Handle Application.OnIdle event. Disadvantage of this approach - yor checking procedure will be runned if nobody click on GUI elements.
Professional way to solve your solution - wrap your variable by complex object, for example:
Trigger = class
private
FOnChanged: TNotifyEvent;
public
procedure Emit;
property OnChanged: TNotifyEvent read FOnChanged write FOnChanged;
end;
procedure Trigger.Emit;
if Assined(FOnChanged) then
FOnChanged(Self)
end;
Cause of your application has not threads we can implement Trigger without mutexes/critical sections, on another side you can handle changing as soon as event producer will raise Emit
Good approach if you don't want use multithreading is split your ligic on multiple state machines based on coroutines.
Example based on AIO framework https://github.com/Purik/AIO
AIO framework create itself events loop, scheduling multiple state machines in parallel without threads:
program TriggerExample;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
SyncObjs,
Gevent,
Greenlets;
const
WAIT_TMEOUT_MSEC = 1000;
var
ChangedEvent: TGevent;
Value: Boolean = False;
// Part of application that raise change events randomly
procedure EventsProducer;
begin
while True do
begin
Greenlets.GreenSleep(100+Random(10000));
Value := True;
ChangedEvent.SetEvent;
end;
end;
begin
ChangedEvent := TGevent.Create(False, False);
// run fake event producer inside other state machine
TSymmetric.Spawn(EventsProducer);
// Loop
while True do
begin
if ChangedEvent.WaitFor(WAIT_TMEOUT_MSEC) = wrSignaled then
begin
WriteLn('Value was changed');
Value := False
end
else
begin
WriteLn('Exit by timeout');
end;
end;
end.

Delphi Pascal XE4 Compiler bug?

I wonder if I have found an Embarcadero compiler bug ...
The problem looks like it is related to generics.
Here is my source code
unit u_DateCount;
interface
uses
SysUtils,
u_JavaScriptable
;
type
TDateCount = class (TJavaScriptable)
strict private
public
NoOfSamples : Integer;
TheDate : TDate;
function ToString():String; override;
end;
implementation
function TDateCount.ToString():String;
var
myYear, myMonth, myDay : Word;
begin
DecodeDate(TheDate, myYear, myMonth, myDay);
Result := Format('[new Date(%d, %d ,0), %d]', [myYear, myMonth, NoOfSamples]);
end;
end.
unit u_Javascriptable;
interface
type
TJavaScriptable = class
strict private
public
function ToString:String; override;
end;
implementation
function TJavaScriptable.ToString:String;
begin
Result := '';
end;
end.
unit u_LineChart;
interface
uses
System.IOUtils,
SysUtils,
System.Generics.Collections,
u_JavaScriptable
;
type
TLineChart<RecordType : TJavaScriptable> = class
strict private
Template : String;
function ConvertRecordsToString():String;
public
Records : TList<RecordType>;
function ToString():String;
constructor Create(templatePath : String);
destructor Destroy(); override;
end;
implementation
function TLineChart<RecordType>.ConvertRecordsToString():String;
var
I: Integer;
begin
//Open brackets
Result := '[ ';
//The first record
if Records.Count > 0 then
begin
Result := Result + Records[0].ToString();
end;
//Loop over records
for I := 1 to Records.Count - 1 do
begin
Result := Result + ', ' + Records[I].ToString();
end;
//Close bracket
Result := Result + ' ]';
end;
function TLineChart<RecordType>.ToString():String;
begin
Result := Format(Template, [ConvertRecordsToString()]);
end;
constructor TLineChart<RecordType>.Create(templatePath : String);
begin
inherited Create();
Template := TFile.ReadAllText(templatePath);
Records := TList<RecordType>.Create();
end;
destructor TLineChart<RecordType>.Destroy();
var
I: Integer;
begin
if Assigned(Records) then
begin
for I := 0 to Records.Count - 1 do
begin
Records[I].Destroy();
end;
Records.Clear();
Records.Destroy();
Records := nil;
end;
inherited;
end;
end.
And finally the main program
program Project4;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
u_Javascriptable in 'u_Javascriptable.pas',
u_LineChart in 'u_LineChart.pas',
u_DateCount in 'u_DateCount.pas';
var
lineChart : TLineChart<TDateCount>;
begin
lineChart := TLineChart<TDateCount>.Create('linechart.html');
try
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
The error message I get when I try to compile this is
[dcc32 Fatal Error] Project4.dpr(30): F2084 Internal Error:
AV097530AC-R00000014-0
Usually when I see an error message similar to this, I can fix it by closing the embarcadero IDE and restarting it. However this did not seem to work this time.
The problem is in the implementation of TLineChart<RecordType>.Destroy().
Change Records[I].Destroy(); to Records[I].Free(); and it works.
Or you just do it correct and use TObjectList<RecordType>.Create; in the constructor which takes care of destroying all elements in it when destroying the list.
Never call Destroy directly. Use Free. While it should not result in a compiler error it is wrong anyway.
If the compiler reports an "internal error," that's always a compiler bug. You should open a ticket in QC for this. Hopefully they can get it fixed for XE5.
Since this works in XE3 but not XE4, I'm going to presume this is an XE4 bug. Until this is fixed, the solution is to use a different version of the compiler such as XE3.

With what delphi Code should I replace my calls to deprecated TThread method Suspend?

It has been asked before, but without a full answer. This is to do with the so called famous "‘Fatal threading model!’".
I need to replace this call to TThread.Suspend with something safe, that returns when terminated or resumed:
procedure TMyThread.Execute;
begin
while (not Terminated) do begin
if PendingOffline then begin
PendingOffline := false; // flag off.
ReleaseResources;
Self.Suspend; // suspend thread. { evil! ask Barry Kelly why.}
// -- somewhere else, after a long time, a user clicks
// a resume button, and the thread resumes: --
if Terminated then
exit; // leave TThread.Execute.
// Not terminated, so we continue..
GrabResources;
end;
end;
end;
The original answer vaguely suggests "TMutex, TEvent and critical sections".
I guess I'm looking for a TThreadThatDoesntSuck.
Here's the sample TThread derivative with a Win32Event, for comments:
unit SignalThreadUnit;
interface
uses
Classes,SysUtils,Windows;
type
TSignalThread = class(TThread)
protected
FEventHandle:THandle;
FWaitTime :Cardinal; {how long to wait for signal}
//FCritSec:TCriticalSection; { critical section to prevent race condition at time of change of Signal states.}
FOnWork:TNotifyEvent;
FWorkCounter:Cardinal; { how many times have we been signalled }
procedure Execute; override; { final; }
//constructor Create(CreateSuspended: Boolean); { hide parent }
public
constructor Create;
destructor Destroy; override;
function WaitForSignal:Boolean; { returns TRUE if signal received, false if not received }
function Active:Boolean; { is there work going on? }
property WorkCounter:Cardinal read FWorkCounter; { how many times have we been signalled }
procedure Sync(AMethod: TThreadMethod);
procedure Start; { replaces method from TThread }
procedure Stop; { provides an alternative to deprecated Suspend method }
property Terminated; {make visible}
published
property WaitTime :Cardinal read FWaitTime write FWaitTime; {how long to wait for signal}
property OnWork:TNotifyEvent read FOnWork write FOnWork;
end;
implementation
{ TSignalThread }
constructor TSignalThread.Create;
begin
inherited Create({CreateSuspended}true);
// must create event handle first!
FEventHandle := CreateEvent(
{security} nil,
{bManualReset} true,
{bInitialState} false,
{name} nil);
FWaitTime := 10;
end;
destructor TSignalThread.Destroy;
begin
if Self.Suspended or Self.Terminated then
CloseHandle(FEventHandle);
inherited;
end;
procedure TSignalThread.Execute;
begin
// inherited; { not applicable here}
while not Terminated do begin
if WaitForSignal then begin
Inc(FWorkCounter);
if Assigned(FOnWork) then begin
FOnWork(Self);
end;
end;
end;
OutputDebugString('TSignalThread shutting down');
end;
{ Active will return true when it is easily (instantly) apparent that
we are not paused. If we are not active, it is possible we are paused,
or it is possible we are in some in-between state. }
function TSignalThread.Active: Boolean;
begin
result := WaitForSingleObject(FEventHandle,0)= WAIT_OBJECT_0;
end;
procedure TSignalThread.Start;
begin
SetEvent(FEventHandle); { when we are in a signalled state, we can do work}
if Self.Suspended then
inherited Start;
end;
procedure TSignalThread.Stop;
begin
ResetEvent(FEventHandle);
end;
procedure TSignalThread.Sync(AMethod: TThreadMethod);
begin
Synchronize(AMethod);
end;
function TSignalThread.WaitForSignal: Boolean;
var
ret:Cardinal;
begin
result := false;
ret := WaitForSingleObject(FEventHandle,FWaitTime);
if (ret=WAIT_OBJECT_0) then
result := not Self.Terminated;
end;
end.
EDIT: Latest version can be found on GitHub: https://github.com/darianmiller/d5xlib
I've come up with this solution as a basis for TThread enhancement with a working Start/Stop mechanism that doesn't rely on Suspend/Resume. I like to have a thread manager that monitors activity and this provides some of the plumbing for that.
unit soThread;
interface
uses
Classes,
SysUtils,
SyncObjs,
soProcessLock;
type
TsoThread = class;
TsoNotifyThreadEvent = procedure(const pThread:TsoThread) of object;
TsoExceptionEvent = procedure(pSender:TObject; pException:Exception) of object;
TsoThreadState = (tsActive,
tsSuspended_NotYetStarted,
tsSuspended_ManuallyStopped,
tsSuspended_RunOnceCompleted,
tsTerminationPending_DestroyInProgress,
tsSuspendPending_StopRequestReceived,
tsSuspendPending_RunOnceComplete,
tsTerminated);
TsoStartOptions = (soRepeatRun,
soRunThenSuspend,
soRunThenFree);
TsoThread = class(TThread)
private
fThreadState:TsoThreadState;
fOnException:TsoExceptionEvent;
fOnRunCompletion:TsoNotifyThreadEvent;
fStateChangeLock:TsoProcessResourceLock;
fAbortableSleepEvent:TEvent;
fResumeSignal:TEvent;
fTerminateSignal:TEvent;
fExecDoneSignal:TEvent;
fStartOption:TsoStartOptions;
fProgressTextToReport:String;
fRequireCoinitialize:Boolean;
function GetThreadState():TsoThreadState;
procedure SuspendThread(const pReason:TsoThreadState);
procedure Sync_CallOnRunCompletion();
procedure DoOnRunCompletion();
property ThreadState:TsoThreadState read GetThreadState;
procedure CallSynchronize(Method: TThreadMethod);
protected
procedure Execute(); override;
procedure BeforeRun(); virtual; // Override as needed
procedure Run(); virtual; ABSTRACT; // Must override
procedure AfterRun(); virtual; // Override as needed
procedure Suspending(); virtual;
procedure Resumed(); virtual;
function ExternalRequestToStop():Boolean; virtual;
function ShouldTerminate():Boolean;
procedure Sleep(const pSleepTimeMS:Integer);
property StartOption:TsoStartOptions read fStartOption write fStartOption;
property RequireCoinitialize:Boolean read fRequireCoinitialize write fRequireCoinitialize;
public
constructor Create(); virtual;
destructor Destroy(); override;
function Start(const pStartOption:TsoStartOptions=soRepeatRun):Boolean;
procedure Stop(); //not intended for use if StartOption is soRunThenFree
function CanBeStarted():Boolean;
function IsActive():Boolean;
property OnException:TsoExceptionEvent read fOnException write fOnException;
property OnRunCompletion:TsoNotifyThreadEvent read fOnRunCompletion write fOnRunCompletion;
end;
implementation
uses
ActiveX,
Windows;
constructor TsoThread.Create();
begin
inherited Create(True); //We always create suspended, user must call .Start()
fThreadState := tsSuspended_NotYetStarted;
fStateChangeLock := TsoProcessResourceLock.Create();
fAbortableSleepEvent := TEvent.Create(nil, True, False, '');
fResumeSignal := TEvent.Create(nil, True, False, '');
fTerminateSignal := TEvent.Create(nil, True, False, '');
fExecDoneSignal := TEvent.Create(nil, True, False, '');
end;
destructor TsoThread.Destroy();
begin
if ThreadState <> tsSuspended_NotYetStarted then
begin
fTerminateSignal.SetEvent();
SuspendThread(tsTerminationPending_DestroyInProgress);
fExecDoneSignal.WaitFor(INFINITE); //we need to wait until we are done before inherited gets called and locks up as FFinished is not yet set
end;
inherited;
fAbortableSleepEvent.Free();
fStateChangeLock.Free();
fResumeSignal.Free();
fTerminateSignal.Free();
fExecDoneSignal.Free();
end;
procedure TsoThread.Execute();
procedure WaitForResume();
var
vWaitForEventHandles:array[0..1] of THandle;
vWaitForResponse:DWORD;
begin
vWaitForEventHandles[0] := fResumeSignal.Handle;
vWaitForEventHandles[1] := fTerminateSignal.Handle;
vWaitForResponse := WaitForMultipleObjects(2, #vWaitForEventHandles[0], False, INFINITE);
case vWaitForResponse of
WAIT_OBJECT_0 + 1: Terminate;
WAIT_FAILED: RaiseLastOSError;
//else resume
end;
end;
var
vCoInitCalled:Boolean;
begin
try
try
while not ShouldTerminate() do
begin
if not IsActive() then
begin
if ShouldTerminate() then Break;
Suspending;
WaitForResume(); //suspend()
//Note: Only two reasons to wake up a suspended thread:
//1: We are going to terminate it 2: we want it to restart doing work
if ShouldTerminate() then Break;
Resumed();
end;
if fRequireCoinitialize then
begin
CoInitialize(nil);
vCoInitCalled := True;
end;
BeforeRun();
try
while IsActive() do
begin
Run(); //descendant's code
DoOnRunCompletion();
case fStartOption of
soRepeatRun:
begin
//loop
end;
soRunThenSuspend:
begin
SuspendThread(tsSuspendPending_RunOnceComplete);
Break;
end;
soRunThenFree:
begin
FreeOnTerminate := True;
Terminate();
Break;
end;
else
begin
raise Exception.Create('Invalid StartOption detected in Execute()');
end;
end;
end;
finally
AfterRun();
if vCoInitCalled then
begin
CoUnInitialize();
end;
end;
end; //while not ShouldTerminate()
except
on E:Exception do
begin
if Assigned(OnException) then
begin
OnException(self, E);
end;
Terminate();
end;
end;
finally
//since we have Resumed() this thread, we will wait until this event is
//triggered before free'ing.
fExecDoneSignal.SetEvent();
end;
end;
procedure TsoThread.Suspending();
begin
fStateChangeLock.Lock();
try
if fThreadState = tsSuspendPending_StopRequestReceived then
begin
fThreadState := tsSuspended_ManuallyStopped;
end
else if fThreadState = tsSuspendPending_RunOnceComplete then
begin
fThreadState := tsSuspended_RunOnceCompleted;
end;
finally
fStateChangeLock.Unlock();
end;
end;
procedure TsoThread.Resumed();
begin
fAbortableSleepEvent.ResetEvent();
fResumeSignal.ResetEvent();
end;
function TsoThread.ExternalRequestToStop:Boolean;
begin
//Intended to be overriden - for descendant's use as needed
Result := False;
end;
procedure TsoThread.BeforeRun();
begin
//Intended to be overriden - for descendant's use as needed
end;
procedure TsoThread.AfterRun();
begin
//Intended to be overriden - for descendant's use as needed
end;
function TsoThread.Start(const pStartOption:TsoStartOptions=soRepeatRun):Boolean;
var
vNeedToWakeFromSuspendedCreationState:Boolean;
begin
vNeedToWakeFromSuspendedCreationState := False;
fStateChangeLock.Lock();
try
StartOption := pStartOption;
Result := CanBeStarted();
if Result then
begin
if (fThreadState = tsSuspended_NotYetStarted) then
begin
//Resumed() will normally be called in the Exec loop but since we
//haven't started yet, we need to do it here the first time only.
Resumed();
vNeedToWakeFromSuspendedCreationState := True;
end;
fThreadState := tsActive;
//Resume();
if vNeedToWakeFromSuspendedCreationState then
begin
//We haven't started Exec loop at all yet
//Since we start all threads in suspended state, we need one initial Resume()
Resume();
end
else
begin
//we're waiting on Exec, wake up and continue processing
fResumeSignal.SetEvent();
end;
end;
finally
fStateChangeLock.Unlock();
end;
end;
procedure TsoThread.Stop();
begin
SuspendThread(tsSuspendPending_StopRequestReceived);
end;
procedure TsoThread.SuspendThread(const pReason:TsoThreadState);
begin
fStateChangeLock.Lock();
try
fThreadState := pReason; //will auto-suspend thread in Exec
fAbortableSleepEvent.SetEvent();
finally
fStateChangeLock.Unlock();
end;
end;
procedure TsoThread.Sync_CallOnRunCompletion();
begin
if Assigned(fOnRunCompletion) then fOnRunCompletion(Self);
end;
procedure TsoThread.DoOnRunCompletion();
begin
if Assigned(fOnRunCompletion) then CallSynchronize(Sync_CallOnRunCompletion);
end;
function TsoThread.GetThreadState():TsoThreadState;
begin
fStateChangeLock.Lock();
try
if Terminated then
begin
fThreadState := tsTerminated;
end
else if ExternalRequestToStop() then
begin
fThreadState := tsSuspendPending_StopRequestReceived;
end;
Result := fThreadState;
finally
fStateChangeLock.Unlock();
end;
end;
function TsoThread.CanBeStarted():Boolean;
begin
Result := (ThreadState in [tsSuspended_NotYetStarted,
tsSuspended_ManuallyStopped,
tsSuspended_RunOnceCompleted]);
end;
function TsoThread.IsActive():Boolean;
begin
Result := (ThreadState = tsActive);
end;
procedure TsoThread.Sleep(const pSleepTimeMS:Integer);
begin
fAbortableSleepEvent.WaitFor(pSleepTimeMS);
end;
procedure TsoThread.CallSynchronize(Method: TThreadMethod);
begin
if IsActive() then
begin
Synchronize(Method);
end;
end;
Function TsoThread.ShouldTerminate():Boolean;
begin
Result := Terminated or
(ThreadState in [tsTerminationPending_DestroyInProgress, tsTerminated]);
end;
end.
To elaborate on the original answer, (and on Smasher's rather short explanation), create a TEvent object. This is a synchronization object that's used for threads to wait on the right time to continue.
You can think of the event object as a traffic light that's either red or green. When you create it, it's not signaled. (Red) Make sure that both your thread and the code that your thread is waiting on have a reference to the event. Then instead of saying Self.Suspend;, say EventObject.WaitFor(TIMEOUT_VALUE_HERE);.
When the code that it's waiting on is finished running, instead of saying ThreadObject.Resume;, you write EventObject.SetEvent;. This turns the signal on (green light) and lets your thread continue.
EDIT: Just noticed an omission above. TEvent.WaitFor is a function, not a procedure. Be sure to check it's return type and react appropriately.
You could use an event (CreateEvent) and let the thread wait (WaitForObject) until the event is signaled (SetEvent). I know that this is a short answer, but you should be able to look these three commands up on MSDN or wherever you want. They should do the trick.
Your code uses a Windows event handle, it should better be using a TEvent from the SyncObjs unit, that way all the gory details will already be taken care of.
Also I don't understand the need for a waiting time - either your thread is blocked on the event or it isn't, there is no need for the wait operation to time out. If you do this to be able to shut the thread down - it's much better to use a second event and WaitForMultipleObjects() instead. For an example see this answer (a basic implementation of a background thread to copy files), you only need to remove the code dealing with file copying and add your own payload. You can easily implement your Start() and Stop() methods in terms of SetEvent() and ResetEvent(), and freeing the thread will properly shut it down.

Resources