Mutley2003
asked on
Queued Threads, but with a responsive GUI
Hi
I have a thread class that goes through all the files on a drive, and calls synchronize to feed information back to the VCL thread. It works fine.
Now, I want to launch several of these threads in sequence, and still have the GUI being responsive. "In sequence" meaning that the nest thread should not start
until the previous thread has finished
like (pseudocode)
procedure buttonScanAllDrives.Click;
begin
ThreadedProcessDrive('C');
waitUntilFinished;
ThreadedProcessDrive('D');
waitUntilFinished;
ThreadedProcessDrive('E');
end;
It is the WaitUntilFinished bit that has me confused. I tried setting a flag in OnTerminate and checking that in a loop with sleep and application.processMessage s - bad news, unresponsive GUI.
Then I tried TThread.WaitFor .. same story
then I tried WaitForSingleObject (between the calls to ThreadedProcessDrive) .. same story.
hmmm .. do I need a supervisor thread that creates the others and resumes them ?
seems like a thread too many
ideas/code anyone?
thanks
I have a thread class that goes through all the files on a drive, and calls synchronize to feed information back to the VCL thread. It works fine.
Now, I want to launch several of these threads in sequence, and still have the GUI being responsive. "In sequence" meaning that the nest thread should not start
until the previous thread has finished
like (pseudocode)
procedure buttonScanAllDrives.Click;
begin
ThreadedProcessDrive('C');
waitUntilFinished;
ThreadedProcessDrive('D');
waitUntilFinished;
ThreadedProcessDrive('E');
end;
It is the WaitUntilFinished bit that has me confused. I tried setting a flag in OnTerminate and checking that in a loop with sleep and application.processMessage
Then I tried TThread.WaitFor .. same story
then I tried WaitForSingleObject (between the calls to ThreadedProcessDrive) .. same story.
hmmm .. do I need a supervisor thread that creates the others and resumes them ?
seems like a thread too many
ideas/code anyone?
thanks
Assign the OnTerminate event of your thread. It is synchronized.
SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
I think MsgWaitForMultipleObjects should be helpfull
procedure buttonScanAllDrives.Click;
var
th: THandle;
begin
th := ThreadedProcessDrive('C');
WaitForThread(th);
th := ThreadedProcessDrive('D');
WaitForThread(th);
th := ThreadedProcessDrive('E');
WaitForThread(th);
end;
procedure WaitForThread(ThreadHandle : THandle);
begin
while MsgWaitForMultipleObjects( 1, ThreadHandle, False, INFINITE, QS_ALLINPUT) = WAIT_OBJECT_0 + 1 do
Application.ProcessMessage s;
end;
procedure buttonScanAllDrives.Click;
var
th: THandle;
begin
th := ThreadedProcessDrive('C');
WaitForThread(th);
th := ThreadedProcessDrive('D');
WaitForThread(th);
th := ThreadedProcessDrive('E');
WaitForThread(th);
end;
procedure WaitForThread(ThreadHandle
begin
while MsgWaitForMultipleObjects(
Application.ProcessMessage
end;
Sorry, messing around with MsgWaitForMultipleObjects on Delph threads is problematic at best. It probably interferes with Delphi thread synchronization.
It is also abusing the idea of threads itself. Why should i wait for a thread? The thread can tell itself when its work is completed.
It is also abusing the idea of threads itself. Why should i wait for a thread? The thread can tell itself when its work is completed.
I think that to keep GUI resposive you need to put "Sleep(50);" after each thread excute iteration (meaning inside thread Execute loop - see below).
procedure TMyThread.Execute;
begin
while (not Terminated) do
begin
//your code
//....
Sleep(50); //Here the thread gives time for the GUI to be responsive
end;
end;
procedure TMyThread.Execute;
begin
while (not Terminated) do
begin
//your code
//....
Sleep(50); //Here the thread gives time for the GUI to be responsive
end;
end;
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
can you create a new thread tMyThreadManager and call the *child* threads from there
Daniel
Daniel
ASKER
Thanks everyone
A few comments
1. Robert, I think your idea of using OnTerminate to start the next thread (in a queue) is very sensible, although the logic could get a bit messy. But of course it is even simpler to hand the whole job (the list of drives to be processed) to a single thread .. dk why I did not think of that..
Anyway, I will try both of those solutions and see how they 'feel' when coded.
2. fwiw, here is some discussion of a "wait thread" that launches multiple threads (simultaneously, not queues) .. I mght be able to adapt that.
http://www.delphicorner.f9.co.uk/articles/op1.htm
3. sas13, your solution 3 (involving WM_THREAD_DONE PostMessage) looks quite good ..
4. Lee, I took some time to work through your code - nice stuff. I have posted an implementation below for anyone who is interested. Please correct me if I have got it wrong , but this is how I understand it.
There is only one thread (TqueueThread) and that executes (in sequence) a list of "tasks" (aka methods) .. these methods are popped on to the queue by the ThreadedMethodQueue (manager) , which the QueueThread is aware of. You can also remove safely tasks from the queue via the queue manager, because these tasks are not executed (popped off the list) until the previous one has been finished. You can add tasks too .. if the thread has not finished, then the extra task just gets pushed onto the internal list, but if it has finished then a new internal list is created. I am not sure how you tell when a task is finished (?PostMessage?)
So this setup should work for ANY "engine" which can be set up like so
Tfred = class(TinterfacedObject, Imethod)
and you need to implement the Execute method of Imethod (not to be confused with the execute method of Tthread .. it could have been called DoTask?)
There are two issues which affect me
* my ProcessDrive class is a descendant of TThread, which is quite handy if I just want to process 1 drive, but won't work in Lee's setup. I could refactor it, I guess.
* I am using synchronize (for each file found).. although PostMessage is often suggested as the better way to go, I have found synchronize to be quite good, and few performance penalties. It enables me to expose the attributes of the found file as properties of the ProcessDriveClass .. something that is more convenient than packaging them up in a record and posting that off to the gui layer.
So, if I don't want to change the architecture too much, maybe I have to go with the less elegant solution of having a supervisor thread which launches and waits for worker threads.
But I am very tempted to work some more with Lee's framework because it is so general purpose. And very cool
Thanks for the help
A few comments
1. Robert, I think your idea of using OnTerminate to start the next thread (in a queue) is very sensible, although the logic could get a bit messy. But of course it is even simpler to hand the whole job (the list of drives to be processed) to a single thread .. dk why I did not think of that..
Anyway, I will try both of those solutions and see how they 'feel' when coded.
2. fwiw, here is some discussion of a "wait thread" that launches multiple threads (simultaneously, not queues) .. I mght be able to adapt that.
http://www.delphicorner.f9.co.uk/articles/op1.htm
3. sas13, your solution 3 (involving WM_THREAD_DONE PostMessage) looks quite good ..
4. Lee, I took some time to work through your code - nice stuff. I have posted an implementation below for anyone who is interested. Please correct me if I have got it wrong , but this is how I understand it.
There is only one thread (TqueueThread) and that executes (in sequence) a list of "tasks" (aka methods) .. these methods are popped on to the queue by the ThreadedMethodQueue (manager) , which the QueueThread is aware of. You can also remove safely tasks from the queue via the queue manager, because these tasks are not executed (popped off the list) until the previous one has been finished. You can add tasks too .. if the thread has not finished, then the extra task just gets pushed onto the internal list, but if it has finished then a new internal list is created. I am not sure how you tell when a task is finished (?PostMessage?)
So this setup should work for ANY "engine" which can be set up like so
Tfred = class(TinterfacedObject, Imethod)
and you need to implement the Execute method of Imethod (not to be confused with the execute method of Tthread .. it could have been called DoTask?)
There are two issues which affect me
* my ProcessDrive class is a descendant of TThread, which is quite handy if I just want to process 1 drive, but won't work in Lee's setup. I could refactor it, I guess.
* I am using synchronize (for each file found).. although PostMessage is often suggested as the better way to go, I have found synchronize to be quite good, and few performance penalties. It enables me to expose the attributes of the found file as properties of the ProcessDriveClass .. something that is more convenient than packaging them up in a record and posting that off to the gui layer.
So, if I don't want to change the architecture too much, maybe I have to go with the less elegant solution of having a supervisor thread which launches and waits for worker threads.
But I am very tempted to work some more with Lee's framework because it is so general purpose. And very cool
Thanks for the help
ASKER
this is an implementation of Lee Nover's ThreadedMethodQueue for those that are interested
uses
..
ThreadedQueue,
MethodIntf;
const
WM_PRIME = WM_APP + 400;
type
TForm1 = class(TForm)
dsMemo1: TdsMemo;
Button1: TButton;
btnRestartThread: TButton;
procedure Button1Click(Sender: TObject);
procedure btnRestartThreadClick(Send er: TObject);
private
protected
procedure ShowPrime(var Message: TMessage); message WM_PRIME;
public
FTMQ: TThreadedMethodQueue;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
type
Tfred = class(TinterfacedObject, Imethod)
private
_StartNum: Integer;
_WndHandle: Thandle;
protected
function IsPrime(const val: integer): Boolean;
public
constructor create(const startNum: Integer; const wndHandle: Thandle);
procedure Execute;
end;
procedure TForm1.Button1Click(Sender : TObject);
begin
FTMQ := TThreadedMethodQueue.creat e;
FTMQ.Push(Tfred.create(900 000, self.Handle));
FTMQ.Push(Tfred.create(400 000, self.Handle));
end;
procedure TForm1.btnRestartThreadCli ck(Sender: TObject);
begin
FTMQ.Push(Tfred.create(600 000, self.Handle));
end;
{ Tfred }
constructor Tfred.create(const startNum: Integer; const wndHandle: Thandle);
begin
_StartNum := startNum;
_WndHandle := wndHandle;
end;
procedure Tfred.Execute;
// find 50 primes from _startnum
var
kk, kkk: integer;
begin
kk := 0;
kkk := _Startnum;
while TRUE do begin
inc(kkk);
if not IsPrime(kkk) then continue;
inc(kk);
PostMessage(_WndHandle, WM_PRIME, kk, kkk);
if kk >= 50 then break;
end;
end;
function Tfred.IsPrime(const val: integer): Boolean;
// deliberately inefficient
var
k : integer; x: extended;
begin
result := false;
for k := 2 to val - 1 do begin
if (val mod k) = 0 then exit;
x := k;
if sqrt(x * (x + 1.0)) < k - 1 then exit; // waste some cycles
end;
result := true;
end;
{ __________________________ __________ __________ __________ ___ }
procedure TForm1.ShowPrime(var Message: TMessage);
begin
dsmemo1.out([Message.lpara m, Message.wparam]);
end;
end.
uses
..
ThreadedQueue,
MethodIntf;
const
WM_PRIME = WM_APP + 400;
type
TForm1 = class(TForm)
dsMemo1: TdsMemo;
Button1: TButton;
btnRestartThread: TButton;
procedure Button1Click(Sender: TObject);
procedure btnRestartThreadClick(Send
private
protected
procedure ShowPrime(var Message: TMessage); message WM_PRIME;
public
FTMQ: TThreadedMethodQueue;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
type
Tfred = class(TinterfacedObject, Imethod)
private
_StartNum: Integer;
_WndHandle: Thandle;
protected
function IsPrime(const val: integer): Boolean;
public
constructor create(const startNum: Integer; const wndHandle: Thandle);
procedure Execute;
end;
procedure TForm1.Button1Click(Sender
begin
FTMQ := TThreadedMethodQueue.creat
FTMQ.Push(Tfred.create(900
FTMQ.Push(Tfred.create(400
end;
procedure TForm1.btnRestartThreadCli
begin
FTMQ.Push(Tfred.create(600
end;
{ Tfred }
constructor Tfred.create(const startNum: Integer; const wndHandle: Thandle);
begin
_StartNum := startNum;
_WndHandle := wndHandle;
end;
procedure Tfred.Execute;
// find 50 primes from _startnum
var
kk, kkk: integer;
begin
kk := 0;
kkk := _Startnum;
while TRUE do begin
inc(kkk);
if not IsPrime(kkk) then continue;
inc(kk);
PostMessage(_WndHandle, WM_PRIME, kk, kkk);
if kk >= 50 then break;
end;
end;
function Tfred.IsPrime(const val: integer): Boolean;
// deliberately inefficient
var
k : integer; x: extended;
begin
result := false;
for k := 2 to val - 1 do begin
if (val mod k) = 0 then exit;
x := k;
if sqrt(x * (x + 1.0)) < k - 1 then exit; // waste some cycles
end;
result := true;
end;
{ __________________________
procedure TForm1.ShowPrime(var Message: TMessage);
begin
dsmemo1.out([Message.lpara
end;
end.
you got it right ! tnx :)
you can also pass the file properties in message parameters
if you're using TSearchRec you could post the data like:
PostMessage(FReceiver, UM_FILE_SEARCH, FILE_FOUND, Integer(@LSR)); // where LSR: TSearchRec
and handle it like:
PSearchRec(Message.LParam) ^....
> I am not sure how you tell when a task is finished
yes I just post a proper message like:
PostMessage(FReceiver, UM_FILE_SEARCH, SEARCH_DONE, 0);
you could still implement the IMethod interface in your thread class
TMyThread = class(TThread, IInterface, IMethod)
protected
IMethod.Execute = DoTask;
procedure DoTask;
procedure Execute; override;
...+ handle the IInterface methods like they are in TInterfacedObject
you'd create your thread suspended, DoTask would:
Resume;
WaitFor(INFINITE);
but I advise against this because you can easily lock up your threads :)
you can also pass the file properties in message parameters
if you're using TSearchRec you could post the data like:
PostMessage(FReceiver, UM_FILE_SEARCH, FILE_FOUND, Integer(@LSR)); // where LSR: TSearchRec
and handle it like:
PSearchRec(Message.LParam)
> I am not sure how you tell when a task is finished
yes I just post a proper message like:
PostMessage(FReceiver, UM_FILE_SEARCH, SEARCH_DONE, 0);
you could still implement the IMethod interface in your thread class
TMyThread = class(TThread, IInterface, IMethod)
protected
IMethod.Execute = DoTask;
procedure DoTask;
procedure Execute; override;
...+ handle the IInterface methods like they are in TInterfacedObject
you'd create your thread suspended, DoTask would:
Resume;
WaitFor(INFINITE);
but I advise against this because you can easily lock up your threads :)
ASKER
Thanks Lee and everyone.
I am going to give most of the points to Lee (as if he needs them <g>) because of the elegance of his solution. However, I got a good result just using a manager thread (with waitfor in between the child threads) .. as I mooted in the original question and danielluyo also suggested. I still think that Robert's suggestion of launching the next thread in the OnTerminate would work fine too.
thanks everyone
I am going to give most of the points to Lee (as if he needs them <g>) because of the elegance of his solution. However, I got a good result just using a manager thread (with waitfor in between the child threads) .. as I mooted in the original question and danielluyo also suggested. I still think that Robert's suggestion of launching the next thread in the OnTerminate would work fine too.
thanks everyone
tnx :)
good solutions get hardly recognized here
good solutions get hardly recognized here