4

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?

alancc
  • 487
  • 2
  • 24
  • 68
  • 5
    I always cry a little whenever I see someone use `Application.ProcessMessages` and wonder why things aren't working properly. – Jerry Dodge Sep 18 '13 at 18:09
  • 5
    Put it in a thread. Give up on `ProcessMessages`. – David Heffernan Sep 18 '13 at 18:11
  • 1
    Agreed - it needs a thread. I can appreciate the pain of refactoring a complex legacy application, though, certainly. The last time I did this it was the only way to parallelize a previously procedural algorithm. It meant extracting hundreds upon hundreds of global variables from dozens of units, refactoring over a hundred methods spanning nearly ten thousand lines of code... line by line. Including debugging, it took months. I can understand wanting to cower under a quick one-liner fix like `ProcessMessages` - my advice is just don't. It's never worth it in the long run. Fix it. – J... Sep 18 '13 at 18:27
  • 7
    That said... `if I mod 1000 = 0 then...` is the usual suspect in patching this crime. – J... Sep 18 '13 at 18:30

5 Answers5

11

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.

J...
  • 30,968
  • 6
  • 66
  • 143
11

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;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 3
    +1 Never heard of `GetQueueStatus`. Thanks for teaching me that one. Ooh, repeat until False, not sure I've seen that one before, swimming against the tide there!! ;-) – David Heffernan Sep 18 '13 at 20:14
  • If using multi-thread is a good method, then can I keep the DoTask in the main thread for the sake of simplity. And create a new thread which will call Application.ProcessMessages to keep the GUI response to the user inputs? – alancc Sep 19 '13 at 02:15
  • No. `Application.ProcessMessages()` processes the message queue of the thread that calls it. If you call it in a worker thread, messages for the main thread will not be processed while `DoTask()` is running, leaving you square one back at your original problem. The solution is to not block the main thread at all. Do your work in a separate thread, don't even wait on it. Start it, and let it tell you when it is finished. Let the main thread do what it should do - service the UI only. When a UI is concerned, learn to program asynchronously. – Remy Lebeau Sep 19 '13 at 04:51
  • Hi, Remy Lebeau, I read your thread carefully and have the following questions: 1. If I put a button and in its click event, I call MyThread.Terminate, like this: procedure OnButton1Click() begin MyThread.Terminate; end; then the Terminated flag of the thread will become true and the following codeline in the loop will execute, is that true? if Terminated then Exit; 2. In the following codelines: MyThread.Terminate; MyThread.WaitFor; MyThread.Free; what is the usage of MyThread.Terminate; and WaitFor before calling Free? I think the thread is already terminated. – alancc Sep 25 '13 at 16:26
  • 3. How can I send a customized message from MyThread to the main form so that it can response with that message? 4. If I write Execute as follows: procedure TMyTaskThread.Execute; begin // Calling a time-consuming function in DLL CalTimeCOnsumingFunctionInDLL(MyFile); end; then want to create the TMyTaskThread once, but call Execute several times, how to implement that? Is the following code works? MyThread := TMyTaskThread.Create(); try for I := 0 to 20 do begin MyThread.MyFile := MyFileList.Items[I]; MyThread.Execute(); end; finally MyThread.Free; end; – alancc Sep 25 '13 at 16:27
  • `TThread.Terminate()` sets the `TThread.Terminated` property to True and does nothing else. It is the responsibility of your `Execute()` method to check the `Terminated` property periodically and exit as soon as possible. `TThread.WaitFor()` waits for the thread to actually finish terminating itself. You need to do that before freeing the `TThread` object, do not attempt to free a thread that is still running. It is safe to call `Terminate()` and `WaitFor()` on a thread that is already terminated (provided you have not set `TThread.FreeOnTerminate` to True - but that is another discussion). – Remy Lebeau Sep 25 '13 at 17:49
  • You can use `TThread.Synchronize()` or `TThread.Queue()` to have the thread run blocks of code in the context of the main thread. Or the thread can post/send window messages to any HWND that is running in the main thread. – Remy Lebeau Sep 25 '13 at 17:52
  • `Execute()` cannot be called multiple times on a single thread (and *DO NOT* call `Execute()` yourself. The thread calls it for you). Once `Execute()` exits, the thread is terminated and cannot be restarted. If you want the same thread to do something multiple times, you will have to implement a loop inside of `Execute()`. If you want to influence what each iteration of that loop does from outside of the thread, you will need to implement a queuing system inside of the thread that you can push requests to and the thread can loop through the queue as needed. – Remy Lebeau Sep 25 '13 at 17:54
  • Your questions are moving outside the scope of this discussion's original question (how to keep the main thread responsive while a task is busy). New questions need to be asked as separate posts. – Remy Lebeau Sep 25 '13 at 17:57
10

Application.ProcessMessages should be avoided. It can cause all sorts of strange things to your program. A must read: The Dark Side of Application.ProcessMessages in Delphi Applications.

In your case a thread is the solution, even though DoTask() may have to be refactored a bit to run in a thread.

Here is a simple example using an anonymous thread. (Requires Delphi-XE or newer).

uses
  System.Classes;

procedure TForm1.MyButtonClick( Sender : TObject);
var
  aThread : TThread;
begin
  aThread :=
    TThread.CreateAnonymousThread(
      procedure
      var
        I: Integer;
      begin
        // time-consuming loop
        For I := 0 to 1024 * 65536 do
        Begin
          if TThread.CurrentThread.CheckTerminated then
            Break;
          DoTask();
        End;
      end
    );
  // Define a terminate thread event call
  aThread.OnTerminate := Self.TaskTerminated;
  aThread.Start;
  // Thread is self freed on terminate by default
end;

procedure TForm1.TaskTerminated(Sender : TObject);
begin
  // Thread is ready, inform the user
end;

The thread is self destroyed, and you can add a OnTerminate call to a method in your form.

LU RD
  • 34,438
  • 5
  • 88
  • 296
  • 2
    This assumes that `DoTask` is threadsafe. In my experience (here talking about legacy code) it's generally not the task of creating the thread that is the difficult part but systematically refactoring everything inside `DoTask` to make it thread-safe. Unless we know what the contents of `DoTask` are, this could very well be a dangerous proposition. – J... Sep 18 '13 at 18:52
  • @J...; The `Application.ProcessMessages` can also cause all sorts of errors, so avoiding refactoring and putting in `Application.ProcessMessages` can be a bad move. – LU RD Sep 18 '13 at 18:57
  • 2
    @DavidHeffernan ...do tell? What if it's filled with UI calls, chart updates, touches globals that the main thread can mess with once control returns, etc, etc? – J... Sep 18 '13 at 18:58
  • 1
    @LURD any re-entrance issues with `Application.ProcessMessages` will also exist with a threaded model. This part always gets me - threading won't make most of the problems that `ProcessMessages` causes go away - you still have to handle concurrency in both cases and in both cases it can get you into trouble. Adding a thread, imo, keeps the concurrency problems of `ProcessMessages` and also adds cross-thread caveats as well. If anything, I think you have to be more careful with threads than you do with `ProcessMessages`. – J... Sep 18 '13 at 19:01
  • The really bad thing about `ProcessMessages` is that it encourages (or at least does not punish) the writing of very badly structured code. It's not about "weird things" happening... – J... Sep 18 '13 at 19:02
  • 1
    @J... Everybody using `Application.ProcessMessages` should read read this first: [`The Dark Side of Application.ProcessMessages in Delphi Applications`](http://delphi.about.com/od/objectpascalide/a/delphi-processmessages-dark-side.htm). – LU RD Sep 18 '13 at 19:07
  • Agreed, they should read it. At the same time, if you put the same code in a thread then instead of one instance pre-empting the other and corrupting the result you end up with two threads simultaneously trying to read/write to the same variables and also corrupting the result. Concurrency cannot be ignored either with threads or with `ProcessMessages`. I'm not defending the use of `ProcessMessages` here, mind you - it's foul and should be banished to the same hell as `goto`, but it's disingenuous to say that threads magically take care of concurrency/re-entrance problems. They don't. – J... Sep 18 '13 at 19:09
  • @J... The time to refactor the code to use a thread is well spent. – LU RD Sep 18 '13 at 19:13
  • @LURD I agree completely. I've done it many times and it is always worth it. My only point was that OP really needs to be careful about what is inside DoTask before throwing it wholesale into a background thread. At the same time, I also work in a business with heavy time pressure. Sometimes I have to fix legacy code *now* and sometimes I don't have time to refactor until maybe three months from now, especially if it means a heavy refactor. Morphine is ok if it's only for the week after surgery... one just has to be careful not to develop a habit, so to speak. – J... Sep 18 '13 at 19:24
  • 2
    @J... That's what I would term thread affinity. I generally think of threadsafe in terms of races. – David Heffernan Sep 18 '13 at 19:35
  • @DavidHeffernan Ah, I may be being sloppy with terminology. In any case, whatever we want to call it, I maintain that we don't know at this point that the contents of `DoTask` are safe to execute in a different thread without modification. – J... Sep 18 '13 at 19:57
  • 1
    @J... Well, I was just as sloppy. And I habitually lecture people on the fact that threadsafe is imprecise. Anyway you make a very good point. Very likely that `DoTask` has affinity to main thread. – David Heffernan Sep 18 '13 at 20:11
0

Calling Application.ProcessMessages at every iteration will indeed slow down performance, and calling it every few times doesn't always work well if you can't predict how long each iteration will take, so I typically will use GetTickCount to time when 100 milliseconds have passed (1). This is long enough to not slow down performance too much, and fast enough to make the application appear responsive.

var
  tc:cardinal;
begin
  tc:=GetTickCount;
  while something do
   begin
    if cardinal(GetTickCount-tc)>=100 then
     begin
      Application.ProcessMessages;
      tc:=GetTickCount;
     end;
    DoSomething;
   end;
end;

(1): not exactly 100 milliseconds, but somewhere close. There are more precise ways to measure time like QueryPerformanceTimer, but again this is more work and may hinder performance.

Stijn Sanders
  • 35,982
  • 11
  • 45
  • 67
  • 1
    QueryPerformanceCounter is quick enough I think. Wrapped up very nicely by `System.Diagnostics.TStopwatch`. – David Heffernan Sep 18 '13 at 20:12
  • Don't forget to take into account that `GetTickCount()` wraps back to 0 every 49.7 days that Windows has been running continuously, so it is not safe to simply subtract the current `GetTickCount` from the previous one without checking for wrapping first. Otherwise, use `GetTickCount64()` instead, which does not suffer from wrapping. – Remy Lebeau Sep 18 '13 at 20:13
  • GetTickCount was fine in Windows 95 when it was impossible to keep a machine up for that long, but along came XP SP2 and changed all that .... Anyway, no need for GTC64 here, test for wrapping is simple enough. – David Heffernan Sep 18 '13 at 22:17
  • @Remy: notice the explicit cast to cardinal? even if a wrap occurs between GetTickCount and tc, and with time only going forward, you're sure to get a reasonable value. – Stijn Sanders Sep 20 '13 at 08:02
  • @StijnSanders: If `tc` is initialized before the 49.7 day limit is reached, and then `GetTickCount()` is called after the limit has been exceeded and wrapping occured, casting is not going to give you a reasonable result. `tc` will still be a high value near `MAX_DWORD`, and subsequent ticks will be low values near 0, and thus subtracting the two will result in very high values, even if only a few milliseconds actually elapsed, until you reset `tc` with a new lower value. The necessary calculation is different depending on whether wrapping occurred between consecutive ticks. – Remy Lebeau Sep 20 '13 at 19:28
  • Have you calculated 49.7 days in milliseconds? It is 2^32, which is `MAX_DWORD`. And I subtract *from* GetTickCount, so over a wrap I still get a correct number of milliseconds. – Stijn Sanders Sep 22 '13 at 10:05
  • I am well aware that 49.7 days in ms is the same as `MAX_DWORD`. You are assuming the compiler will calculate a result correctly when cardinal overflowing is involved. That is not a good idea to assume that it always works correctly on all systems. It takes 2 extra lines of code to account for tick wrapping. Better to be safe than sorry. – Remy Lebeau Sep 25 '13 at 18:13
0

@user2704265, when you mention “application will lose responses to the end user”, do you mean that you want your user to continue working around in your application clicking and typing away? In that case - heed the previous answers and use threading.

If it’s good enough to provide feedback that your application is busy with a lengthy operation [and hasn't frozen] there are some options you can consider:

  • Dissable user input
  • Change the cursor to “busy”
  • Use a progressbar
  • Add a cancel button

Abiding to your request for a single threaded solution I recommend you start by disabling user input and change the cursor to “busy”.

procedure TForm1.ButtonDoLengthyTaskClick(Sender: TObject);
var i, j : integer;
begin
  Screen.Cursor := crHourGlass;
  //Disable user input capabilities
  ButtonDoLengthyTask.Enabled := false;

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

      // Calling Processmessages marginally increases the process time
      // If we don't call and user clicks the disabled button while waiting then
      // at the end of ButtonDoLengthyTaskClick the proc will be called again
      // doubling the execution time.

      Application.ProcessMessages;

    End;
  Finally
    Screen.Cursor := crDefault;
    ButtonDoLengthyTask.Enabled := true;
  End;
End;
Lars
  • 1,428
  • 1
  • 13
  • 22