Link to home
Start Free TrialLog in
Avatar of Mutley2003
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.processMessages - 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



Avatar of robert_marquardt
robert_marquardt

Assign the OnTerminate event of your thread. It is synchronized.
SOLUTION
Avatar of robert_marquardt
robert_marquardt

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
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.ProcessMessages;
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.
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;
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
can you create a new thread tMyThreadManager and call the *child* threads from there

Daniel
Avatar of Mutley2003

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




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(Sender: 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.create;
  FTMQ.Push(Tfred.create(900000, self.Handle));
  FTMQ.Push(Tfred.create(400000, self.Handle));
end;

procedure TForm1.btnRestartThreadClick(Sender: TObject);
begin
  FTMQ.Push(Tfred.create(600000, 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.lparam, Message.wparam]);
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 :)
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



tnx :)
good solutions get hardly recognized here