Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delphi: How to prevent a single thread app from losing responses?

Tags:

delphi

I am developing a single thread app with Delphi, which will do a time-consuming task, like this:

// time-consuming loop
For I := 0 to 1024 * 65536 do
Begin
    DoTask();
End;

When the loop starts, the application will lose responses to the end user. That is not very good. I also do not want to convert it into a multi-thread application because of its complexity, so I add Application.ProcessMessages accordingly,

// time-consuming loop
For I := 0 to 1024 * 65536 do
Begin
DoTask();
Application.ProcessMessages;
End;

However, this time although the application will response to user operations, the time-consumed in the loop is much more than the original loop, about 10 times.

Is there a solution to make sure the application does not lose the response while do not increase the consumed time too much?

like image 405
alancc Avatar asked Nov 27 '22 05:11

alancc


2 Answers

You really should use a worker thread. This is what threads are good for.

Using Application.ProcessMessages() is a band-aid, not a solution. Your app will still be unresponsive while DoTask() is doing its work, unless you litter DoTask() with additional calls to Application.ProcessMessages(). Plus, calling Application.ProcessMessages() directly introduces reentrant issues if you are not careful.

If you must call Application.ProcessMessages() directly, then don't call it unless there are messages actually waiting to be processed. You can use the Win32 API GetQueueStatus() function to detect that condition, for example:

// time-consuming loop
For I := 0 to 1024 * 65536 do
Begin
  DoTask();
  if GetQueueStatus(QS_ALLINPUT) <> 0 then
    Application.ProcessMessages;
End;

Otherwise, move the DoTask() loop into a thread (yeah yeah) and then have your main loop use MsgWaitForMultipleObjects() to wait for the task thread to finish. That still allows you to detect when to process messages, eg:

procedure TMyTaskThread.Execute;
begin
  // time-consuming loop
  for I := 0 to 1024 * 65536 do
  begin
    if Terminated then Exit;
    DoTask();
  end;
end;

var
  MyThread: TMyTaskThread;
  Ret: DWORD;
begin
  ...
  MyThread := TMyTaskThread.Create;
  repeat
    Ret := MsgWaitForMultipleObjects(1, Thread.Handle, FALSE, INFINITE, QS_ALLINPUT);
    if (Ret = WAIT_OBJECT_0) or (Ret = WAIT_FAILED) then Break;
    if Ret = (WAIT_OBJECT_0+1) then Application.ProcessMessages;
  until False;
  MyThread.Terminate;
  MyThread.WaitFor;
  MyThread.Free;
  ...
end;
like image 170
Remy Lebeau Avatar answered Dec 10 '22 23:12

Remy Lebeau


You say :

I also do not want to convert it into a multi-thread application because of its complexity

I can take this to mean one of two things :

  1. Your application is a sprawling mess of legacy code that is so huge and so badly written that encapsulating DoTask in a thread would mean an enormous amount of refactoring for which a viable business case cannot be made.
  2. You feel that writing multithreaded code is too "complex" and you don't want to learn how to do it.

If the case is #2 then there is no excuse whatsoever - multithreading is the clear answer to this problem. It's not so scary to roll a method into a thread and you'll become a better developer for learning how to do it.

If the case is #1, and I leave this to you to decide, then I'll note that for the duration of the loop you will be calling Application.ProcessMessages 67 million times with this :

For I := 0 to 1024 * 65536 do
Begin
  DoTask();
  Application.ProcessMessages;
End;

The typical way that this crime is covered up is simply by not calling Application.ProcessMessages every time you run through the loop.

For I := 0 to 1024 * 65536 do
Begin
  DoTask();
  if I mod 1024 = 0 then Application.ProcessMessages;
End;

But if Application.ProcessMessages is actually taking ten times longer than DoTask() to execute then I really question how complex DoTask really is and whether it really is such a hard job to refactor it into a thread. If you fix this with ProcessMessages, you really should consider it a temporary solution.

Especially take care that using ProcessMessages means that you must make sure that all of your message handlers are re-entrant.

like image 39
J... Avatar answered Dec 11 '22 00:12

J...