I have a TButton in the main TForm. When user click the button, it will execute the below process:
begin
Process_done := FALSE;
Process_Result.Clear;
cmdProcess.CommandLine := #34+AppPath+'getdata.exe"';
cmdProcess.Run;
Repeat
Application.ProcessMessages;
Until Process_done;
end;
As you can see above, the process calls external executable, and the process can take some times which blocking the main application.
This is only one process, and I need another one.
So, I am thinking to implement multi-threading, where I can run the above process in a separate thread. The other process as well. And the main thread can do something WHILE checking when both processes done.
Can anyone give me some examples how to do this using Delphi 7?
OR point me to an article, simple implementation like this?
Thanks.
Try something like this:
type
TRunProcessThread = class(TThread)
protected
cmdProcess: Whatever;
procedure Execute; override;
public
constructor Create(const ACmdLine: String);
destructor Destroy; override;
end;
constructor TRunProcessThread.Create(const ACmdLine: String);
begin
inherited Create(True);
FreeOnTerminate := True;
cmdProcess := Whatever.Create;
cmdProcess.CommandLine := ACmdLine;
end;
destructor TRunProcessThread.Destroy;
begin
cmdProcess.Free;
inherited;
end;
procedure TRunProcessThread.Execute;
begin
cmdProcess.Run;
...
end;
.
procedure TForm1.Button1Click(Sender: TObject);
var
Thread: TRunProcessThread;
begin
Thread := TRunProcessThread.Create(AnsiQuotedStr(AppPath + 'getdata.exe', #34));
Thread.OnTerminate := ProcessDone;
Thread.Resume;
end;
procedure TForm1.ProcessDone(Sender: TObject);
begin
// access TRunProcessThread(Sender) to get result information as needed ...
end;
You should create a class inherited from TThread and put that code in there. I don't remember exactly, but I think you'll find TThread template in File->New dialog box. When code execution is finished, you just notify your gui. Here's an article how to synchronize UI with external thread http://delphi.about.com/od/kbthread/a/thread-gui.htm
Related
I'm using the TThread.DoTerminate method to notify to the main thread which the TThread has terminated. but as soon try to change the properties of some controls (buttons) from inside of the DoTerminate both controls just disappear of the form.
Also when I close the Form I'm getting this message
Project ProjectTest.exe raised exception class EOSError with message
'System Error. Code: 1400. Invalid window handle'.
This is a sample application to reproduce the issue.
type
TFooThread = class;
TFormSample = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
ProgressBar1: TProgressBar;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FooThread : TFooThread;
procedure ThreadIsDone;
public
end;
TFooThread = class(TThread)
private
FForm : TFormSample;
protected
procedure DoTerminate; override;
public
procedure Execute; override;
constructor Create(AForm : TFormSample); reintroduce;
destructor Destroy; override;
end;
var
FormSample: TFormSample;
implementation
{$R *.dfm}
{ TFooThread }
constructor TFooThread.Create(AForm: TFormSample);
begin
inherited Create(False);
FreeOnTerminate := False;
FForm := AForm;
end;
destructor TFooThread.Destroy;
begin
inherited;
end;
procedure TFooThread.DoTerminate;
begin
FForm.ThreadIsDone;
inherited;
end;
procedure TFooThread.Execute;
var
i : Integer;
begin
for i := 1 to 100 do
begin
Synchronize(
procedure
begin
FForm.ProgressBar1.Position := i;
end
);
Sleep(50);
end;
Terminate();
end;
{ TFormSample }
procedure TFormSample.Button1Click(Sender: TObject);
begin
FooThread := TFooThread.Create(Self);
TButton(Sender).Enabled := false;
end;
procedure TFormSample.FormCreate(Sender: TObject);
begin
FooThread := nil;
Button3.Visible := False;
end;
procedure TFormSample.FormDestroy(Sender: TObject);
begin
if (FooThread<>nil) then
begin
if not FooThread.Terminated then
FooThread.WaitFor;
FooThread.Free;
end;
end;
procedure TFormSample.ThreadIsDone;
begin
//this code is executed but the controls are not updated
//both buttons just disappear from the form !!!!
//Also if I remove these lines, no error is raised.
Button2.Visible := False;
Button3.Visible := True;
end;
end.
The question is : How I can update the properties of some VCL control as soon the TThread is finished?
It should be fine to update controls inside DoTerminate (as you are).
DoTerminate runs in the context of the thread. Therefore it is not safe to update controls from that method. The base implementation synchronises a call to the OnTerminate event.
So OnTerminate is already synchronised. And it will be safe to update controls from an OnTerminate event handler.
However, I would be more inclined to not have code inside the thread class calling the form because this creates a circular dependency. Rather have the form assign a handler for the OnTerminateevent. This way code that controls the form will be in the form class. You can do the same with the control updates to indicate thread progress.
FooThread := TFooThread.Create(...);
//WARNING: If you need to do **any**
//initialisation after creating a
//thread, it's better to create it
//in a Suspended state.
FooThread.OnTerminate := ThreadIsDone;
//Of course you'll have to change the signature of ThreadIsDone accordingly.
FooThread.OnProgress := ThreadProgress;
//You'd have to define a suitable callback event on the thread.
//Finally, if the thread started in a suspended state, resume it.
FooThread.Start;
Avoiding circular dependencies is a little more work, but greatly simplifies an application.
David mentions that you can create your thread in a running state. To do so safely you must:
Pass all necessary initialisation information into the constructor.
And inside the constructor perform all initialisation before calling the inherited constructor.
Also you have a mistake in your Execute method:
procedure TFooThread.Execute;
var
i : Integer;
begin
...
Terminate(); //This is pointless.
//All it does is set Terminated := True;
end;
The thread terminates when it exits. All the call to Terminate does is set an internal flag to indicate the thread should terminate. You'd normally write your Execute method as follows:
begin
while not Terminated do
begin
...
end;
end;
Then your form might have a button which calls: FooThread.Terminate();
This will cause your while loop to exit at the end of the current iteration. This allows the thread to exit "gracefully".
Hi I'm doing a code MessageDlgPos running five threads at the same time, the code is this:
type
TMyThread = class(TThread)
protected
procedure Execute; override;
public
text: string;
property ReturnValue;
end;
procedure TMyThread.Execute;
begin
if Terminated then
Exit;
MessageDlgPos(text, mtInformation, [mbOk], 0, 100, 200);
end;
procedure TForm1.btnTestClick(Sender: TObject);
var
LThread: TMyThread;
i: Integer;
begin
For i := 1 to 5 do
begin
LThread := TMyThread(Sender);
try
LThread.text := 'hi';
LThread.FreeOnTerminate := True;
except
LThread.Free;
raise;
end;
LThread.Resume;
end;
end;
The problem is that Delphi XE always returns the following error and does not execute anything:
First chance exception at $ 7524B727. Exception class EAccessViolation with message 'Access violation at address 00D0B9AB. Write of address 8CC38309 '. Process tester.exe (6300)
How do I fix this problem?
As David Heffernan pointed out, MessageDlgPos() cannot safely be called outside of the main UI thread, and you are not managing the thread correctly. Your code needs to look more like this instead:
type
TMyThread = class(TThread)
protected
procedure Execute; override;
public
text: string;
property ReturnValue;
end;
procedure TMyThread.Execute;
begin
// no need to check Terminated here, TThread already
// does that before calling Execute()...
TThread.Synchronize(nil,
procedure
begin
MessageDlgPos(text, mtInformation, [mbOk], 0, 100, 200);
end
);
end;
procedure TForm1.btnTestClick(Sender: TObject);
var
LThread: TMyThread;
i: Integer;
begin
For i := 1 to 5 do
begin
LThread := TMyThread.Create(True);
LThread.text := 'hi';
LThread.FreeOnTerminate := True;
LThread.Start;
end;
end;
I would suggest a slightly different variation:
type
TMyThread = class(TThread)
private
fText: string;
protected
procedure Execute; override;
public
constructor Create(const aText: string); reintroduce;
property ReturnValue;
end;
constructor TMyThread.Create(const aText: string);
begin
inherited Create(False);
FreeOnTerminate := True;
fText := aText;
end;
procedure TMyThread.Execute;
begin
// no need to check Terminated here, TThread already
// does that before calling Execute()...
TThread.Synchronize(nil,
procedure
begin
MessageDlgPos(fText, mtInformation, [mbOk], 0, 100, 200);
end
);
end;
procedure TForm1.btnTestClick(Sender: TObject);
var
i: Integer;
begin
For i := 1 to 5 do
begin
TMyThread.Create('hi');
end;
end;
But either way, if you don't like using TThread.Synchronize() to delegate to the main thread (thus only displaying 1 dialog at a time) then you cannot use MessageDlgPos() at all, since it is only safe to call in the main UI thread. You can use Windows.MessageBox() instead, which can be safely called in a worker thread without delegation (but then you lose the ability to specify its screen position, unless you access its HWND directly by using a thread-local hook via SetWindowsHookEx() to intercept the dialog's creation and discover its HWND):
procedure TMyThread.Execute;
begin
Windows.MessageBox(0, PChar(fText), PChar(Application.Title), MB_OK or MB_ICONINFORMATION);
);
end;
There are many problems. The biggest one is here:
LThread := TMyThread(Sender);
Sender is a button. Casting to a thread is simply wrong and the cause of your exception. Casting a button to a thread doesn't make it so. It's still a button.
You likely mean to create a thread instead.
LThread := TMyThread.Create(True);
You cannot show VCL UI outside the main thread. The call to MessageDlgPos breaks that rule. If you do need to show UI at that point, you'll need to use TThread.Synchronize to have the code execute in the main thread.
Your exception handler makes no sense to me. I think you should remove it.
Resume is deprecated. Use Start instead.
I have multithreaded app and I have a question regarding assigning objects between threads and how to lock them properly.
I defined custom type class and in main thread I create an instance of that type. I would like to assign different objects to a thread, those objects will be used within Execute method of a thread.
type TMyClass = class
private
FData: Integer;
public
property Data: Integer read FData write FData;
end;
TMyThread = class(TThread)
private
FMyObject: TMyObject;
FLock: TCriticalSection;
protected
procedure Execute; override;
public
procedure Lock;
procedure Unlock;
property MyObject: TMyObject read FMyObject write FMyObject;
end;
procedure TMyThread.Lock;
begin
FLock.Acquire;
end;
procedure TMyThread.Unlock;
begin
FLock.Release;
end;
procedure TMyThread.Execute;
begin
while not Terminated do
begin
Lock;
try
if Assigned(FMyObject) then
FMyObject.Data := FMyObject.Data + 1;
finally
Unlock;
end;
end;
end;
from main thread:
var MyObject1, MyObject2: TMyObject;
thOperation: TMyThread;
CurrData1, CurrData2: Integer;
begin
// create two objects
MyObject1 := TMyObject.Create;
MyObject2 := TMyObject.Create;
// create thread(started)
thOperation := TMyThread.Create(false);
thOperation.Lock;
try
thOperation.MyObject := MyObject1;
finally
thOperation.Unlock;
end;
/// .... do some stuff in main thread
thOperation.Lock;
try
CurrData1 := thOperation.MyObject.Data;
finally
Unlock;
end;
// let's assign new object on a running thread
thOperation.Lock;
try
thOperation.MyObject := MyObject2;
finally
thOperation.Unlock;
end;
/// .... do some stuff in main thread again
thOperation.Lock;
try
CurrData2 := thOperation.MyObject.Data;
finally
Unlock;
end;
if CurrData1 <> CurrData2 then ShowMessage('Different result!');
// do cleanup
thOperation.Terminate;
thOperation.WaitFor;
thOperation.Free;
MyObject1.Free;
MyObject2.Free;
end;
Is this approach of locking when assigning different objects to a thread ok?
To answer your question, yes, your approach of using TCriticalSection is ok.
For more information on multithreading, in case you don't have it yet, Google for 'Multithreading - The Delphi way' by Martin Harvey. An excellent article (or should I say book).
I have a Query and open it in my Thread. It works correctly and I don't want to use Synchronize, because Synchronize makes main Form don't response while the Query not complete fetch.
When close the Form blow error shown:
System Error. Code: 1400. Invalid window handle
type
TMyThread = class(TThread)
public
procedure Execute; override;
procedure doProc;
end; { type }
.
.
.
procedure TMyThread.doProc;
begin
Form1.Query1.Open;
end;
procedure TMyThread.Execute;
begin
inherited;
doProc;
end;
.
.
.
procedure TForm1.Button1Click(Sender: TObject);
begin
thrd := TMyThread.Create(True);
thrd.FreeOnTerminate := True;
thrd.Resume;
end;
Note : Query has a lot of record.
The problem is that the VCL is not thread safe.
In order to have the query execute in parallel to all other things going on you'll have to decouple it from the Form.
That means you'll have to create the Query at runtime using code:
type
TMyThread = class(TThread)
private
FQuery: TQuery;
FOnTerminate: TNotifyEvent;
public
constructor Create(AQuery: TQuery);
destructor Destroy; override;
procedure Execute; override;
procedure doProc;
//Add an event handler to do cleanup on termination.
property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
end; { type }
constructor TMyThread.Create(AQuery: TQuery);
begin
inherited Create(True);
FQuery:= AQuery;
end;
procedure TMyThread.doProc;
begin
FQuery1.Open;
Synchronize(
//anonymous method, use a separate procedure in older Delphi versions
procedure
begin
Form1.Button1.Enabled:= true; //reenable the button when we're done.
end
);
end;
procedure TMyThread.Execute;
begin
inherited;
doProc;
end;
destructor TMyThread.Destroy;
begin
if Assigned(FOnterminate) then FOnTerminate(Self);
inherited;
end;
In the OnClick for Button1 you'll do the following:
type
TForm1 = class(TForm)
private
AQuery: TQuery;
...
end; {type}
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Enabled:= false; //disable the button so it cannot be started twice.
thrd.Free;
AQuery:= TQuery.Create;
AQuery.SQL.Text:= .....
thrd := TMyThread.Create(AQuery);
thrd.OnTerminate:= MyTerminationHandler;
thrd.FreeOnTerminate:= False;
thrd.Resume;
end;
Finally assign cleanup code to the termination handler of the thread.
If you destroy the Query in the thread then you cannot use FreeOnTerminate:= true, but you'll have to Free the thread yourself.
procedure TForm1.MyTerminationHandler(Sender: TObject);
begin
FreeAndNil(AQuery);
end;
Warning
This code will only work if you start 1 thread.
If you want start this thread multiple times (i.e. run multiple queries at the same time), you'll have to create an array of threads e.g.:
TQueryThreads = record
MyThread: TMyThread;
MyQuery: TQuery;
constructor Create(SQL: string);
end; {record}
TForm1 = class(TForm)
private
Threads: array of TQueryThreads;
....
end; {TForm1}
Note that this code will not work in the BDE, because that library does not support multiple running queries at the same time
If you want to do that you'll have to use ZEOS or something like that.
As per TLama's suggestion:
I would suggest switching the BDE TQuery component to ADO, or downloading something like ZEOS components. The BDE is very outdated and has a lot of quirks that will never get fixed because it is no longer maintained.
The only issue that remains is cleaning up the connection if Form1 is closed.
If it's your main form it really does not matter because your whole application will go down.
If it's not your main form than you'll need to disable closing the form by filling the OnCanClose handler.
TForm1.CanClose(Sender: TObject; var CanClose: boolean);
begin
CanClose:= thrd.Finished;
end;
You should prevent any action (user and program) in the MainThread without blocking it. This can easily be done by a modal form, that cannot be closed by the user.
The thread can do anything as long as it takes and the final (synchronized) step is to close that modal form.
procedure OpenDataSetInBackground( ADataSet : TDataSet );
var
LWaitForm : TForm;
begin
LWaitForm := TForm.Create( nil );
try
LWaitForm.BorderIcons := []; // no close buttons
TThread.CreateAnonymousThread(
procedure
begin
try
ADataSet.Open;
finally
TThread.Synchronize( nil,
procedure
begin
LWaitForm.Close;
end );
end;
end );
try
LWaitForm.ShowModal;
finally
LWorkThread.Free;
end;
finally
LWaitForm.Free;
end;
end;
But you have to be careful with this and you should never try do start more than one parallel thread with this code unless you really know, what you are doing.
I have created a class that derives from TThread, because I wish to do some async stuff, however to avoid having to create another class, I built the entire thing around that thread class. Not sure if this is good practice or not, and if I cant get this to work, well then I suppose I have no choice but to recode..
The problem: I create the Thread on FormCreate, assign some properties, and I Free it on FormDestroy. In the Thread's constructor, I set FreeOnTerminate = False. When I click on a button on my Form, I Start(); the Thread. Okay, so it runs as expected, an error occurs (expected!), its being passed to my error handling event, and it appears to terminate. I then click the button again, and I get a Cannot call Start on a running or suspended thread error.
How can I finish the thread without freeing it, and enabling me to start it again?
You can't restart a thread once it is finished/terminated. In that case you should just create a new instance if the thread again like you did in FormCreate.
Catch the error in the thread, handle it there and then let the thread continue the work. To handle the error you could simply queue a method to the main thread to report the error, for example. I hope you aren't letting exceptions leave your thread Execute method.
This is the way that I implement it:
procedure TAPIRequest.DoRequest;
begin
FBusy := True;
Resume;
end;
procedure TAPIRequest.Execute;
begin
inherited;
while not Terminated do begin
HttpError := False;
try
Response := HTTP.Post(URL, Params);
ParseResponse;
except
HttpError := True;
end;
if Assigned(OnResponse) then
OnResponse();
FBusy := False;
Suspend;
end;
end;
type
TMyThread = class(TThread)
protected
procedure Execute; override;
end;
var
Form1: TForm1;
MyThread: TMyThread;
Event: TEvent;
procedure TForm1.FormCreate(Sender: TObject);
begin
Event := TEvent.Create(nil,true,false, '');
MyThread := TMyThread.Create(False);
end;
procedure TMyThread.Execute;
begin
while True do
begin
Event.WaitFor(Infinite);
// do something
Event.ResetEvent;
end;
end;
procedure RestartThread;
begin
Event.SetEvent;
// if you need check thread status, wait or run, use here
// if Event.WaitFor(0) = ...(wrTimeout, wrSignaled)
end;