I have the following problem/question.
I have a unit named "myGlobalFunctions.pas".
Inside this unit I have implemented multiple procedures/functions that are used by several projects.
project 1 use this unit
project 3 use this unit
project 6 use this unit
etc
inside "project 1" there is a thread that use functions inside the "global function" unit.
inside project 3 there is no thread but the functions are used.
so far this thread (project1) provide almost no update of the application interface and the update was made AFTER or BEFORE calling a function from "myGlobalFunctions.pas"
like "before start function1"
... the calling
"after function1".
this way I can know what the program is doing.
However now I want to implement inside the "function1" update of the application interface (with synchronize).
I want to reflect in the application interface "processing step1 ... xx records". (there is a while loop there for a dataset).
using Synchronize for "project1" and with normal label1.caption = 'message'; application.process messages for any other project.
is it possible?
how can I do such a thing.
can be Thread Safe ?
tks a lot
Razvan
here is some code to understand better
THREAD UNIT
procedure TThreadSyncronizeProcess.SignalStart;
begin
frmMain.sbMain.Panels[2].Text := 'Syncronizare in desfasurare...'; -- exist all the time
if Assigned(frmSyncronize) then begin -- check if exist this
frmSyncronize.logMain.WriteFeedBackMessage('Pornire syncronizare...', '', EVENTLOG_INFORMATION_TYPE, True);
end;
end;
procedure TThreadSyncronizeProcess.Execute;
var ..... declarations
begin
Synchronize(SignalStart); -- this is normal call within thread update interface
try
try
workSession := TIB_Session.Create(nil);
workDatabase := TIB_Database.Create(workSession);
... creating more components and setup them ...
uSyncronizareFunctions.SetupDatabase(workDatabase, workSession, transactionWrite, transactionRead);
uSyncronizareFunctions.SetupDataSnapConnection(workConnectionRead, providerRead);
if Assigned(frmSyncronize) then begin
uSyncronizareFunctions.SetupFeedBack(frmSyncronize.logMain);
end;
try
Synchronize(SignalMessage);
// this next function is from the "global unit"
isAllOk := uSyncronizareFunctions.ImportOperatoriAutorizati(workImage, workLabelProgress, True);
isAllOk := isAllOk and uSyncronizareFunctions.ImportJudete;
isAllOk := isAllOk and uSyncronizareFunctions.ImportLocalitati;
isAllOk := isAllOk and uSyncronizareFunctions.ImportUM;
isAllOk := isAllOk and uSyncronizareFunctions.ImportFurnizori;
isAllOk := isAllOk and uSyncronizareFunctions.ImportClasificari;
except
on e : Exception do begin
raise Exception.Create(dmMain.GetDataSnapExceptionMessage(e.Message));
end;
end;
except
on e : Exception do begin
baseMessage := e.Message;
Synchronize(SignalMessage);
end;
end;
finally
workDatabase.ForceDisconnect;
FreeAndNil(transactionRead);
... etc
end;
Synchronize(SignalFinish);
end;
global function unit
unit uSyncronizareFunctions;
function ImportOperatoriAutorizati(imgDone : TImage; labelProgress : TLabel; isThread : Boolean) : Boolean;
var workQuery : TIB_Query;
serverData : TClientDataSet;
begin
Result := True;
try
... create all that we need
serverData.Close;
serverData.CommandText := 'SELECT * FROM OPERATORI_AUTORIZATI WHERE REC_VERSION > :ARECVERSION ORDER BY REC_VERSION, ID';
serverData.Params.Clear;
serverData.Params.CreateParam(ftInteger, 'ARECVERSION', ptInput);
serverData.Params.ParamByName('ARECVERSION').AsInteger := lastVersion;
serverData.Active := True;
...... I want here to signal start
while not serverData.Eof do begin
try
globalInsert_Tran.StartTransaction;
workQuery.Close;
workQuery.ParamByName('AIDGLOBAL').AsString := serverData.FieldByName('IDGLOBAL').AsString;
workQuery.Open;
if workQuery.IsEmpty then begin
workQuery.Insert;
workQuery.FieldByName('IDGLOBAL').AsString := serverData.FieldByName('IDGLOBAL').AsString;
end else begin
workQuery.Edit;
end;
workQuery.FieldByName('NUME').AsString := serverData.FieldByName('NUME').AsString;
workQuery.FieldByName('COD_AUTORIZARE').AsString := serverData.FieldByName('COD_AUTORIZARE').AsString;
workQuery.FieldByName('OTHER_INFO').AsString := serverData.FieldByName('OTHER_INFO').AsString;
workQuery.FieldByName('DATASTERGERE').AsVariant := GetValueDate(serverData.FieldByName('DATASTERGERE').AsDateTime);
workQuery.FieldByName('REC_VERSION').AsInteger := serverData.FieldByName('REC_VERSION').AsInteger;
workQuery.Post;
MarkRecordAsDirtyFalse(workQuery);
globalInsert_Tran.Commit;
...... I want here to signal progress and to see in the application interface "processing record xx/100" or any other message
except
on e : Exception do begin
Result := False;
globalInsert_Tran.Rollback;
end;
end;
serverData.Next;
end;
finally
FreeAndNil(serverData);
FreeAndNil(workQuery);
end;
end;
It looks like you would like your global function to execute a callback. You might try an approach like this:
unit MyGlobalMethods;
interface
uses
System.SysUtils;
type
// define a method signature for your callback
TSomeCallback = procedure(progress : integer) of object;
// add a callback argument to your function (initializing to nil will make
// the parameter optional and will not break your previous implementations)
function GlobalFunction(arg1 : integer;
AMethodCallback : TSomeCallback = nil) : boolean;
implementation
function GlobalFunction(arg1 : integer;
AMethodCallback : TSomeCallback) : boolean;
var i : integer;
begin
for i := 0 to arg1 do begin
sleep(10); // Do some work
// report progress by executing the callback method
// only do this if a method has been passed as argument
if (i mod 100 = 0) and (Assigned(AMethodCallback)) then AMethodCallback(i);
end;
result := true;
end;
end.
Adding a method callback as an argument allows you to pass in any function you like to have the method execute. For example :
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
procedure Button1Click(Sender: TObject);
private
procedure UpdateProgress(progress : integer);
end;
TSomeThread = class(TThread)
private
FProgressCallback : TSomeCallback;
FProgress : integer;
procedure SynchronizeCallback(progress : integer);
procedure DoCallback;
public
procedure Execute; override;
property OnFunctionProgress : TSomeCallback
read FProgressCallback write FProgressCallback;
end;
implement as :
procedure TSomeThread.Execute;
begin
GlobalFunction(1000, SynchronizeCallback);
end;
procedure TSomeThread.SynchronizeCallback(progress: Integer);
begin
FProgress := progress;
Synchronize(DoCallback);
end;
procedure TSomeThread.DoCallback;
begin
if Assigned(FProgressCallback) then FProgressCallback(FProgress);
end;
You haven't told us what version of Delphi you are using. If you are using D2009 or newer you can bundle the above two calls into one using anonymous methods (and get rid of FProgress) :
procedure TSomeThread.SynchronizeCallback(progress: Integer);
begin
Synchronize(procedure
begin
if Assigned(FProgressCallback) then FProgressCallback(progress);
end;);
end;
Where in your form you would do :
procedure TForm1.UpdateProgress(progress: Integer);
begin
label1.Caption := IntToStr(progress);
end;
procedure TForm1.Button1Click(Sender: TObject);
var someThread : TSomeThread;
begin
someThread := TSomeThread.Create(true);
someThread.FreeOnTerminate := true;
someThread.OnFunctionProgress := UpdateProgress;
someThread.Start;
end;
This nicely separates the responsibilities. The main form passes an update method to the thread (a method, in this case, to update a label). The thread is responsible for synchronizing the call and the global function, therefore, does not need to care whether or not the callback it is executing originates from the main thread or from any other thread. The thread knows it needs to synchronize the method so it should take that responsibility.
Related
I am using this in one of my solution
My requirement is to clear the queue and kill all the threads gracefully when Stop button is clicked.
For this I created an ObjectList
var
List: TObjectList<TMyConsumerItem>;
begin
{ Create a new List. }
List := TObjectList<TMyConsumerItem>.Create();
Later I made this modification:
procedure TForm1.DoSomeJob(myListItems: TStringList);
...
for i := 1 to cThreadCount do
List.Add(TMyConsumerItem.Create(aQueue, aCounter));
And on Stop button button click I am doing this
for i := 0 to List.Count - 1 do
begin
List.Item[i].Terminate;
end;
aCounter.Free;
aQueue.Free;
While doing this I application is getting hanged. Is this the correct approach or am I missing something?
I am using 10.2 Tokyo
Edit 1:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TMyConsumerItem = class(TThread)
private
FQueue : TThreadedQueue<TProc>;
FSignal : TCountDownEvent;
protected
procedure Execute; override;
public
constructor Create( aQueue : TThreadedQueue<TProc>; aSignal : TCountdownEvent);
end;
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
procedure StopClick(Sender: TObject);
private
{ Private declarations }
List: TObjectList<TMyConsumerItem>;
aQueue: TThreadedQueue<TProc>;
aCounter: TCountDownEvent;
procedure DoSomeJob( myListItems : TStringList);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
SyncObjs, Generics.Collections;
{- Include TMyConsumerItem class here }
procedure TForm1.Button1Click(Sender: TObject);
var
aList : TStringList;
i : Integer;
begin
aList := TStringList.Create;
Screen.Cursor := crHourGlass;
try
for i := 1 to 20 do aList.Add(IntToStr(i));
DoSomeJob(aList);
finally
aList.Free;
Screen.Cursor := crDefault;
end;
end;
procedure TForm1.StopClick(Sender: TObject);
begin
for i := 0 to List.Count - 1 do
begin
List.Item[i].Terminate;
end;
List.Free;
aCounter.WaitFor;
aCounter.Free;
aQueue.Free;
end;
procedure TForm1.DoSomeJob(myListItems: TStringList);
const
cThreadCount = 10;
cMyQueueDepth = 100;
var
i: Integer;
function CaptureJob(const aString: string): TProc;
begin
Result :=
procedure
var
i,j : Integer;
begin
// Do some job with aString
for i := 0 to 1000000 do
j := i;
// Report status to main thread
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add('Job with:'+aString+' done.');
end
);
end;
end;
var
aThread : TThread;
begin
List := TObjectList<TMyConsumerItem>.Create();
List.OwnsObjects := False;
aQueue := TThreadedQueue<TProc>.Create(cMyQueueDepth);
aCounter := TCountDownEvent.Create(cThreadCount);
try
for i := 1 to cThreadCount do
List.Add(TMyConsumerItem.Create(aQueue, aCounter));
for i := 0 to myListItems.Count - 1 do
begin
aQueue.PushItem(CaptureJob(myListItems[i]));
end;
finally
end;
end;
constructor TMyConsumerItem.Create(aQueue: TThreadedQueue<TProc>; aSignal : TCountDownEvent);
begin
Inherited Create(false);
Self.FreeOnTerminate := true;
FQueue := aQueue;
FSignal := aSignal;
end;
procedure TMyConsumerItem.Execute;
var
aProc : TProc;
begin
try
repeat
FQueue.PopItem(aProc);
aProc();
until Terminated;
finally
FSignal.Signal;
end;
end;
end.
You left out some important stuff regarding how the job queue works and how to interact with the threadpool.
Taking a reference to a thread that is self-terminating is wrong. Remove the List, since it is useless.
In order to finish the queue at a later point, make aQueue global.
To finish the threadpool, add as many empty tasks to the queue as there are threads.
See example below how a stop method could be implemented. Note that both aCounter and aQueue must be global in scope. Disclaimer untested, not in front of a compiler at the moment.
If you need to abort ongoing work in the job tasks, you will have to provide a reference to a global (in scope) flag with each job task, and signal to end the task.
There are other libraries that can perform similar work, see Delphi PPL or the well proven OTL library.
procedure TForm1.StopClick(Sender: TObject);
var
i : Integer;
aThread : TThread;
begin
// Kill the worker threads by pushing nil
for i := 1 to cThreadCount do
aQueue.PushItem(nil);
// Since the worker threads synchronizes with the main thread,
// we must wait for them in another thread.
aThread := TThread.CreateAnonymousThread(
procedure
begin
aCounter.WaitFor; // Wait for threads to finish
aCounter.Free;
aQueue.Free;
end
);
aThread.FreeOnTerminate := false;
aThread.Start;
aThread.WaitFor; // Safe to wait for the anonymous thread
aThread.Free;
end;
Terminate only sets the Terminated property to true. It's important that the internal loop of the thread checks the Terminated property periodically, and returns from the Execute method when it is set to true. After that, use WaitFor in the main thread to check the threads have all ended before you free queue or thread-pool objects.
I am designing a thread pool with following features.
New thread should be spawned only when all other threads are running.
Maximum number of thread should be configurable.
When a thread is waiting, it should be able to handle new requests.
Each IO operation should call a callback on completion
Thread should have a way to manage request its serving and IO callbacks
Here is the code:
unit ThreadUtilities;
interface
uses
Windows, SysUtils, Classes;
type
EThreadStackFinalized = class(Exception);
TSimpleThread = class;
// Thread Safe Pointer Queue
TThreadQueue = class
private
FFinalized: Boolean;
FIOQueue: THandle;
public
constructor Create;
destructor Destroy; override;
procedure Finalize;
procedure Push(Data: Pointer);
function Pop(var Data: Pointer): Boolean;
property Finalized: Boolean read FFinalized;
end;
TThreadExecuteEvent = procedure (Thread: TThread) of object;
TSimpleThread = class(TThread)
private
FExecuteEvent: TThreadExecuteEvent;
protected
procedure Execute(); override;
public
constructor Create(CreateSuspended: Boolean; ExecuteEvent: TThreadExecuteEvent; AFreeOnTerminate: Boolean);
end;
TThreadPoolEvent = procedure (Data: Pointer; AThread: TThread) of Object;
TThreadPool = class(TObject)
private
FThreads: TList;
fis32MaxThreadCount : Integer;
FThreadQueue: TThreadQueue;
FHandlePoolEvent: TThreadPoolEvent;
procedure DoHandleThreadExecute(Thread: TThread);
procedure SetMaxThreadCount(const pis32MaxThreadCount : Integer);
function GetMaxThreadCount : Integer;
public
constructor Create( HandlePoolEvent: TThreadPoolEvent; MaxThreads: Integer = 1); virtual;
destructor Destroy; override;
procedure Add(const Data: Pointer);
property MaxThreadCount : Integer read GetMaxThreadCount write SetMaxThreadCount;
end;
implementation
constructor TThreadQueue.Create;
begin
//-- Create IO Completion Queue
FIOQueue := CreateIOCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
FFinalized := False;
end;
destructor TThreadQueue.Destroy;
begin
//-- Destroy Completion Queue
if (FIOQueue = 0) then
CloseHandle(FIOQueue);
inherited;
end;
procedure TThreadQueue.Finalize;
begin
//-- Post a finialize pointer on to the queue
PostQueuedCompletionStatus(FIOQueue, 0, 0, Pointer($FFFFFFFF));
FFinalized := True;
end;
function TThreadQueue.Pop(var Data: Pointer): Boolean;
var
A: Cardinal;
OL: POverLapped;
begin
Result := True;
if (not FFinalized) then
//-- Remove/Pop the first pointer from the queue or wait
GetQueuedCompletionStatus(FIOQueue, A, Cardinal(Data), OL, INFINITE);
//-- Check if we have finalized the queue for completion
if FFinalized or (OL = Pointer($FFFFFFFF)) then begin
Data := nil;
Result := False;
Finalize;
end;
end;
procedure TThreadQueue.Push(Data: Pointer);
begin
if FFinalized then
Raise EThreadStackFinalized.Create('Stack is finalized');
//-- Add/Push a pointer on to the end of the queue
PostQueuedCompletionStatus(FIOQueue, 0, Cardinal(Data), nil);
end;
{ TSimpleThread }
constructor TSimpleThread.Create(CreateSuspended: Boolean;
ExecuteEvent: TThreadExecuteEvent; AFreeOnTerminate: Boolean);
begin
FreeOnTerminate := AFreeOnTerminate;
FExecuteEvent := ExecuteEvent;
inherited Create(CreateSuspended);
end;
Changed the code as suggested by J... also added critical sections but the problem i am facing now is that when i am trying call multiple task only one thread is being used, Lets say if i added 5 threads in the pool then only one thread is being used which is thread 1. Please check my client code as well in the below section.
procedure TSimpleThread.Execute;
begin
// if Assigned(FExecuteEvent) then
// FExecuteEvent(Self);
while not self.Terminated do begin
try
// FGoEvent.WaitFor(INFINITE);
// FGoEvent.ResetEvent;
EnterCriticalSection(csCriticalSection);
if self.Terminated then break;
if Assigned(FExecuteEvent) then
FExecuteEvent(Self);
finally
LeaveCriticalSection(csCriticalSection);
// HandleException;
end;
end;
end;
In the Add method, how can I check if there is any thread which is not busy, if it is not busy then reuse it else create a new thread and add it in ThreadPool list?
{ TThreadPool }
procedure TThreadPool.Add(const Data: Pointer);
begin
FThreadQueue.Push(Data);
// if FThreads.Count < MaxThreadCount then
// begin
// FThreads.Add(TSimpleThread.Create(False, DoHandleThreadExecute, False));
// end;
end;
constructor TThreadPool.Create(HandlePoolEvent: TThreadPoolEvent;
MaxThreads: Integer);
begin
FHandlePoolEvent := HandlePoolEvent;
FThreadQueue := TThreadQueue.Create;
FThreads := TList.Create;
FThreads.Add(TSimpleThread.Create(False, DoHandleThreadExecute, False));
end;
destructor TThreadPool.Destroy;
var
t: Integer;
begin
FThreadQueue.Finalize;
for t := 0 to FThreads.Count-1 do
TThread(FThreads[t]).Terminate;
while (FThreads.Count = 0) do begin
TThread(FThreads[0]).WaitFor;
TThread(FThreads[0]).Free;
FThreads.Delete(0);
end;
FThreadQueue.Free;
FThreads.Free;
inherited;
end;
procedure TThreadPool.DoHandleThreadExecute(Thread: TThread);
var
Data: Pointer;
begin
while FThreadQueue.Pop(Data) and (not TSimpleThread(Thread).Terminated) do begin
try
FHandlePoolEvent(Data, Thread);
except
end;
end;
end;
function TThreadPool.GetMaxThreadCount: Integer;
begin
Result := fis32MaxThreadCount;
end;
procedure TThreadPool.SetMaxThreadCount(const pis32MaxThreadCount: Integer);
begin
fis32MaxThreadCount := pis32MaxThreadCount;
end;
end.
Client Code :
This the client i created to log the data in text file :
unit ThreadClient;
interface
uses Windows, SysUtils, Classes, ThreadUtilities;
type
PLogRequest = ^TLogRequest;
TLogRequest = record
LogText: String;
end;
TThreadFileLog = class(TObject)
private
FFileName: String;
FThreadPool: TThreadPool;
procedure HandleLogRequest(Data: Pointer; AThread: TThread);
public
constructor Create(const FileName: string);
destructor Destroy; override;
procedure Log(const LogText: string);
procedure SetMaxThreadCount(const pis32MaxThreadCnt : Integer);
end;
implementation
(* Simple reuse of a logtofile function for example *)
procedure LogToFile(const FileName, LogString: String);
var
F: TextFile;
begin
AssignFile(F, FileName);
if not FileExists(FileName) then
Rewrite(F)
else
Append(F);
try
Writeln(F, DateTimeToStr(Now) + ': ' + LogString);
finally
CloseFile(F);
end;
end;
constructor TThreadFileLog.Create(const FileName: string);
begin
FFileName := FileName;
//-- Pool of one thread to handle queue of logs
FThreadPool := TThreadPool.Create(HandleLogRequest, 5);
end;
destructor TThreadFileLog.Destroy;
begin
FThreadPool.Free;
inherited;
end;
procedure TThreadFileLog.HandleLogRequest(Data: Pointer; AThread: TThread);
var
Request: PLogRequest;
los32Idx : Integer;
begin
Request := Data;
try
for los32Idx := 0 to 100 do
begin
LogToFile(FFileName, IntToStr( AThread.ThreadID) + Request^.LogText);
end;
finally
Dispose(Request);
end;
end;
procedure TThreadFileLog.Log(const LogText: string);
var
Request: PLogRequest;
begin
New(Request);
Request^.LogText := LogText;
FThreadPool.Add(Request);
end;
procedure TThreadFileLog.SetMaxThreadCount(const pis32MaxThreadCnt: Integer);
begin
FThreadPool.MaxThreadCount := pis32MaxThreadCnt;
end;
end.
This is the form application where i added three buttons, each button click will write some value to the file with thread id and text msg. But the problem is thread id is always same
unit ThreadPool;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ThreadClient;
type
TForm5 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure Edit1Change(Sender: TObject);
private
{ Private declarations }
fiFileLog : TThreadFileLog;
public
{ Public declarations }
end;
var
Form5: TForm5;
implementation
{$R *.dfm}
procedure TForm5.Button1Click(Sender: TObject);
begin
fiFileLog.Log('Button one click');
end;
procedure TForm5.Button2Click(Sender: TObject);
begin
fiFileLog.Log('Button two click');
end;
procedure TForm5.Button3Click(Sender: TObject);
begin
fiFileLog.Log('Button three click');
end;
procedure TForm5.Edit1Change(Sender: TObject);
begin
fiFileLog.SetMaxThreadCount(StrToInt(Edit1.Text));
end;
procedure TForm5.FormCreate(Sender: TObject);
begin
fiFileLog := TThreadFileLog.Create('C:/test123.txt');
end;
end.
First, and probably most strongly advisable, you might consider using a library like OmniThread to implement a threadpool. The hard work is done for you and you will likely end up making a substandard and buggy product with a roll-your-own solution. Unless you have special requirements this is probably the fastest and easiest solution.
That said, if you want to try to do this...
What you might consider is to just make all of the threads in your pool at startup rather than on-demand. If the server is going to busy at any point then it will eventually end up with a pool of MaxThreadCount soon enough anyway.
In any case, if you want to keep a pool of threads alive and available for work then they would need to follow a slightly different model than what you have written.
Consider:
procedure TSimpleThread.Execute;
begin
if Assigned(FExecuteEvent) then
FExecuteEvent(Self);
end;
Here when you run your thread it will execute this callback and then terminate. This doesn't seem to be what you want. What you seem to want is to keep the thread alive but waiting for its next work package. I use a base thread class (for pools) with an execute method that looks something like this (this is somewhat simplified):
procedure TMyCustomThread.Execute;
begin
while not self.Terminated do begin
try
FGoEvent.WaitFor(INFINITE);
FGoEvent.ResetEvent;
if self.Terminated then break;
MainExecute;
except
HandleException;
end;
end;
end;
Here FGoEvent is a TEvent. The implementing class defines what the work package looks like in the abstract MainExecute method, but whatever it is the thread will perform its work and then return to waiting for the FGoEvent to signal that it has new work to do.
In your case, you need to keep track of which threads are waiting and which are working. You will probably want a manager class of some sort to keep track of these thread objects. Assigning something simple like a threadID to each one seems sensible. For each thread, just before launching it, make a record that it is currently busy. At the very end of your work package you can then post a message back to the manager class telling it that the work is done (and that it can flag the thread as available for work).
When you add work to the queue you can first check for available threads to run the work (or create a new one if you wish to follow the model you outlined). If there are threads then launch the task, if there are not then push the work onto the work queue. When worker threads report complete the manager can check the queue for outstanding work. If there is work it can immediately re-deploy the thread. If there isn't work it can flag the thread as available for work (here you might use a second queue for available workers).
A full implementation is too complex to document in a single answer here - this aims just to rough out some general ideas.
I have a running threading application that is computing some longer calculations.
procedure TForm.calculationInThread(value: Integer);
var aThread : TThread;
begin
aThread :=
TThread.CreateAnonymousThread(
procedure
begin
myCalculation(value);
end
);
aThread.FreeOnTerminate := True;
aThread.OnTerminate := self.calculationInThreadEnd;
aThread.Start;
end;
And an implementation of calculationInThreadEnd;
procedure TForm.calculationInThreadEnd(Sender: TObject);
begin
doSomething;
end;
I may miss just something stupid, but how to pass a value to calculationInThreadEnd? I found
TThread.SetReturnValue(value);
but how do i access that in the onTerminate call?
Solution
type THackThread = class(TThread);
procedure TForm1.calculationInThreadEnd(Sender: TObject);
var Value: Integer;
begin
Value := THackThread(Sender as TThread).ReturnValue;
end;
The Sender parameter of the OnTerminate event is the thread object. So you can do this:
aThread :=
TThread.CreateAnonymousThread(
procedure
begin
myCalculation(value);
TThread.SetReturnValue(...);
end
);
Then in the OnTerminate event handler you can do:
procedure TForm.calculationInThreadEnd(Sender: TObject);
var
Value: Integer;
begin
Value := (Sender as TThread).ReturnValue;
end;
Update
The return value property is protected so you'll need to use the protected hack to gain access to it.
Any ideas why I get this warning in Delphi XE:
[DCC Warning] Form1.pas(250): W1036 Variable '$frame' might not have been initialized
procedure TForm1.Action1Execute(Sender: TObject);
var
Thread: TThread;
begin
...
Thread := TThread.CreateAnonymousThread(
procedure{Anonymos}()
procedure ShowLoading(const Show: Boolean);
begin /// <------------- WARNING IS GIVEN FOR THIS LINE (line number 250)
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
...
Button1.Enabled := not Show;
...
end
);
end;
var
i: Integer;
begin
ShowLoading(true);
try
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
... // some UI updates
end
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
... // some UI updates
end
);
finally
ShowLoading(false);
end;
end
).NameThread('Some Thread Name');
Thread.Start;
end;
I do not have anywhere in my code a variable names frame nor $frame. I am even not sure how $frame with $ sign can be a valid identifier.
Smells like compiler magic to me.
PS: Of course the real life xosw is having other than Form1, Button1, Action1 names.
In XE2 this compiler warning is not there.
Anyway, to make your method a bit clearer to the compiler, try this :
procedure TForm1.Action1Execute(Sender : TObject);
var
Thread: TThread;
begin
Thread := TThread.CreateAnonymousThread(
procedure{Anonymos}()
Type
TProcRef = reference to procedure (const Show: Boolean);
var
i: Integer;
ShowLoading : TProcRef;
begin
ShowLoading:=
procedure (const Show: Boolean)
begin
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
// Button1.Enabled := not Show;
end
);
end;
ShowLoading(true);
... // and so on.
Update :
I just checked in XE, this really fixes the compiler warning.
The reference I gave in the comment to the question explains about the warning and frame objects.
So I guess that the XE compiler is getting confused about the frame object ShowLoading is creating. And creating an explicit reference fixed the warning.
Update 2 :
Just showing my test for your reference :
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,Classes, SysUtils;
procedure Action1Execute;
var
Thread: TThread;
begin
Thread := TThread.CreateAnonymousThread(
procedure{Anonymos}()
Type
TProcRef = reference to procedure (const Show: Boolean);
var
i: Integer;
ShowLoading : TProcRef;
begin
ShowLoading:=
procedure (const Show: Boolean)
begin
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
// Button1.Enabled := not Show;
end
);
end;
ShowLoading(true);
try
Thread.Synchronize( Thread,
procedure{Anonymous}()
begin
// some UI updates
end
);
Thread.Synchronize( Thread,
procedure{Anonymous}()
begin
// some UI updates
end
);
finally
ShowLoading(false);
end;
end
); //.NameThreadForDebugging('Some Thread Name');
Thread.Start;
end;
begin
try
{ TODO -oUser -cConsole Main : Insert code here }
Action1Execute;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
I am trying to write to copy a file by invoking a separate thread.
Here is my form code:
unit frmFileCopy;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ComCtrls, StdCtrls;
type
TForm2 = class(TForm)
Button3: TButton;
procedure Button3Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
private
ThreadNumberCounter : integer;
procedure HandleTerminate (Sender: Tobject);
end;
var
Form2: TForm2;
implementation
uses
fileThread;
{$R *.dfm}
{ TForm2 }
const
sourcePath = 'source\'; //'
destPath = 'dest\'; //'
fileSource = 'bigFile.zip';
fileDest = 'Copy_bigFile.zip';
procedure TForm2.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
CanClose := true;
if ThreadNumberCounter >0 then
begin
if MessageDlg('The file is being copied. Do you want to quit?', mtWarning,
[mbYes, mbNo],0) = mrNo then
CanClose := false;
end;
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
ThreadNumberCounter := 0;
end;
procedure TForm2.Button3Click(Sender: TObject);
var
sourceF, destF : string;
copyFileThread : TCopyThread;
begin
sourceF := ExtractFilePath(ParamStr(0)) + sourcePath + fileSource;
destF := ExtractFilePath(ParamStr(0)) + sourcePath + fileDest;
copyFileThread := TCopyThread.create(sourceF,destF);
copyFileThread.FreeOnTerminate := True;
try
Inc(ThreadNumberCounter);
copyFileThread.Execute;
copyFileThread.OnTerminate := HandleTerminate;
copyFileThread.Resume;
except
on Exception do
begin
copyFileThread.Free;
ShowMessage('Error in thread');
end;
end;
end;
procedure TForm2.HandleTerminate(Sender: Tobject);
begin
Dec(ThreadNumberCounter);
end;
Here is my class:
unit fileThread;
interface
uses
Classes, SysUtils;
type
TCopyThread = class(TThread)
private
FIn, FOut : string;
procedure copyfile;
public
procedure Execute ; override;
constructor create (const source, dest : string);
end;
implementation
{ TCopyThread }
procedure TCopyThread.copyfile;
var
streamSource, streamDest : TFileStream;
bIn, bOut : byte;
begin
streamSource := TFileStream.Create(FIn, fmOpenRead);
try
streamDest := TFileStream.Create(FOut,fmCreate);
try
streamDest.CopyFrom(streamSource,streamSource.Size);
streamSource.Position := 0;
streamDest.Position := 0;
{check file consinstency}
while not (streamSource.Position = streamDest.Size) do
begin
streamSource.Read(bIn, 1);
streamDest.Read(bOut, 1);
if bIn <> bOut then
raise Exception.Create('files are different at position' +
IntToStr(streamSource.Position));
end;
finally
streamDest.Free;
end;
finally
streamSource.Free;
end;
end;
constructor TCopyThread.create(const source, dest: string);
begin
FIn := source;
FOut := dest;
end;
procedure TCopyThread.Execute;
begin
copyfile;
inherited;
end;
end.
When I run the application, I received a following error:
Project prjFileCopyThread raised exception class EThread with message: 'Cannot call Start on a running or suspended thread'.
I do not have experience with threads.
I use Martin Harvey's tutorial as a guide, but any advice how to improve it make safe thread would be appreciated.
Based on the answers, I've changed my code. This time it worked. I would appreciate if you can review it again and tell what should be improved.
procedure TForm2.Button3Click(Sender: TObject);
var
sourceF, destF : string;
copyFileThread : TCopyThread;
begin
sourceF := ExtractFilePath(ParamStr(0)) + sourcePath + fileSource;
destF := ExtractFilePath(ParamStr(0)) + destPath + fileDest;
copyFileThread := TCopyThread.create;
try
copyFileThread.InFile := sourceF;
copyFileThread.OutFile := destF;
except
on Exception do
begin
copyFileThread.Free;
ShowMessage('Error in thread');
end;
end;
Here is my class:
type
TCopyThread = class(TThread)
private
FIn, FOut : string;
procedure setFin (const AIN : string);
procedure setFOut (const AOut : string);
procedure FCopyFile;
protected
procedure Execute ; override;
public
constructor Create;
property InFile : string write setFin;
property OutFile : string write setFOut;
end;
implementation
{ TCopyThread }
procedure TCopyThread.FCopyfile;
var
streamSource, streamDest : TFileStream;
bIn, bOut : byte;
begin
{removed the code to make it shorter}
end;
procedure TCopyThread.setFin(const AIN: string);
begin
FIn := AIN;
end;
procedure TCopyThread.setFOut(const AOut: string);
begin
FOut := AOut;
end;
constructor TCopyThread.create;
begin
FreeOnTerminate := True;
inherited Create(FALSE);
end;
procedure TCopyThread.Execute;
begin
FCopyfile;
end;
end.
You have a few problems:
You don't call inherited Create. In this case, since you want to do things first and start it yourself, you should use
inherited Create(True); // Creates new thread suspended.
You should never call Execute yourself. It's called automatically if you create non-suspended, or if you call Resume.
There is no inherited Execute, but you call it anyway.
BTW, you could also use the built-in Windows Shell function SHFileOperation to do the copy. It will work in the background, handles multiple files and wildcards, and can automatically display progress to the user. You can probably find an example of using it in Delphi here on SO; here is a link for using it to recursively delete files, for example.
A good search here on SO is (without the quotes) shfileoperation [delphi]
Just for comparison - that's how you'd do it with OmniThreadLibrary.
uses
OtlCommon, OtlTask, OtlTaskControl;
type
TForm3 = class(TForm)
...
FCopyTask: IOmniTaskControl;
end;
procedure BackgroundCopy(const task: IOmniTask);
begin
CopyFile(PChar(string(task.ParamByName['Source'])), PChar(string(task.ParamByName['Dest'])), true);
//Exceptions in CopyFile will be mapped into task's exit status
end;
procedure TForm3.BackgroundCopyComplete(const task: IOmniTaskControl);
begin
if task.ExitCode = EXIT_EXCEPTION then
ShowMessage('Exception in copy task: ' + task.ExitMessage);
FCopyTask := nil;
end;
procedure TForm3.Button3Click(Sender: TObject);
begin
FCopyTask := CreateOmniTask(BackgroundCopy)
.SetParameter('Source', ExtractFilePath(ParamStr(0)) + sourcePath + fileSource)
.SetParameter('Dest', ExtractFilePath(ParamStr(0)) + destPath + fileDest)
.SilentExceptions
.OnTerminate(BackgroundCopyComplete)
.Run;
end;
procedure TForm3.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
CanClose := true;
if assigned(FCopyTask) then
begin
if MessageDlg('The file is being copied. Do you want to quit?', mtWarning,
[mbYes, mbNo],0) = mrNo then
CanClose := false
else
FCopyTask.Terminate;
end;
end;
Your edited code still has at least two big problems:
You have a parameterless constructor, then set the source and destination file names by means of thread class properties. All you have been told about creating suspended threads not being necessary holds true only if you do all setup in the thread constructor - after this has finished thread execution will begin, and access to thread properties need to be synchronized. You should (as indeed your first version of the code did) give both names as parameters to the thread. It's even worse: the only safe way to use a thread with the FreeOnTerminate property set is to not access any property once the constructor has finished, because the thread may have destroyed itself already, or could do while the property is accessed.
In case of an exception you free the thread object, even though you have set its FreeOnTerminate property. This will probably result in a double free exception from the memory manager.
I do also wonder how you want to know when the copying of the file is finished - if there is no exception the button click handler will exit with the thread still running in the background. There is also no means of cancelling the running thread. This will cause your application to exit only when the thread has finished.
All in all you would be better off to use one of the Windows file copying routines with cancel and progress callbacks, as Ken pointed out in his answer.
If you do this only to experiment with threads - don't use file operations for your tests, they are a bad match for several reasons, not only because there are better ways to do the same in the main thread, but also because I/O bandwidth will be used best if no concurrent operations are attempted (that means: don't try to copy several files in parallel by creating several of your threads).
The Execute method of a thread is normally not explicitly called by client code. In other words: delete CopyFileThread.Execute in unit frmFileCopy. The thread is started when the Resume method is invoked.
Also in unit fileThread in the constructor of TCopyThread inherited Create(True) should be called as first to create a thread in suspended state.
You execute the thread and then trying to Resume it while it is running.
copyFileThread.Execute;
copyFileThread.OnTerminate := HandleTerminate;
copyFileThread.Resume;