views:

218

answers:

5

I've got some long but simple loops in my Delphi program that may loop millions of times and take some seconds to execute. The code inside of the loop is very fast and has been optimized. It just takes long because it is done so many times.

e.g.:

Screen.Cursor = crHourGlass;
R := FirstRecord;
while R <> nil do begin
  { do something simple with R.Value }
  R := R.NextRecord;
end;
Screen.Cursor := crDefault;

Now I don't want my program to be non-responsive, so I want to add an Application.ProcessMessages inside the loop. But I also want the added statements to slow down my loop as little as possible.

I am following a linked list, so I don't even have a counting variable available and would have to add one if I wanted intervals. Or I'd have to add a timer, but need to minimize the time checking.

How should I implement this to minimize the overhead that's added?


Conclusion:

For now, I'm doing something like APZ28's answer.

But it looks like long term I should implement some sort of threading to handle this. Thanks for pointing this out, because I thought that Application.ProcessMessages was the only way to do it.

+9  A: 

Can you put the work loop in a thread, freeing up the main thread for the GUI loop processing.

gbrandt
@gbrandt: In some cases, yes. But in others, the main thread (and the user) has to wait for the work to get done.
lkessler
I don't think you have many options. You'll get the best performance using threads, the easiest implementation using Application.ProcessMessages. Pick One.
Lieven
@lkessler: That is not a reason to skip threads. You can for example put your loop in an asynchronous call (for example using AsyncCalls, http://andy.jgknet.de/blog/?page_id=100) and show a modal progress dialog in the GUI thread that can optionally allow to cancel the action. Writing a boolean in the main thread and reading it in the worker can be done without synchronization, so there's no overhead at all. With interlocked increments the worker could likewise update a loop counter without really affecting throughput.
mghie
@Lieven: Never, ever, EVER use Application.ProcessMessages for this sort of thing - unless you go to lengths to prevent it, it creates re-entrancy issues in your application. Application.ProcessMessages is not a solution to *any* problem - it is merely a *cause* of problems. Rather than using ProcessMessages to simulate threading (it existed in Delphi 1 because there was no other way) you should do threading "properly"; with real threads). It might seem like more work in the short term, but Application.ProcessMessages will cost you more in the long run - guaranteed.
Deltics
@Deltics: with given example, protecting against re-entrance would be easy enough but if it makes you feel any better, my preference goes to threading.
Lieven
@Lieven: It's not a question of preference - Proper threading and Application.ProcessMessages are too dissimilar to be a question of preferring one over the other. That would be like saying you "prefer" putting butter on a burn than rather than ice or cold water - one [the latter] is the proper treatment, the other makes matters worse. You should simply never advise Application.ProcessMessages *especially* as a "simple" solution.
Deltics
@Deltics: here I have to disagree. The *problem* you try to solve is the same, only the *implementation* differs, each with its own set of problems. I already agreed that threading is the way to go but it would be wrong to dismiss ProcessMessages. If I'd get a dollar (euro in my case:) for every application using ProcessMessages without problems I probably could retire.
Lieven
@Lieven: After moving on from Delphi 1, I have never seen an Application using Application.ProcessMessages that did not suffer re-entrancy issues caused by it's use. Very often those "problems" were trivial UI "glitches" that confused users with some strange behaviours, only in the worst cases were things like AV's caused, and when they were, reproducing them was next to impossible because of the nature of the problem that re-entrancy causes. I can only conclude that you only deal with very trivial apps or are still working with Delphi 1.
Deltics
@Lieven: Yes, the problem is the same: maintaining a responsive UI. Application.ProcessMessages addresses that by creating multiple potential entry points into the event handling framework of an application, interleaved with some other processing with no synchronisation between the two. Threading places non-UI processing in it's own thread and synchronisation is *required* to maintain communication between the thread and the UI. So yes they both *address* the problem, but one is a solution while the other merely *hides* the problem (and creates potential new ones in the process).
Deltics
@Deltics: *I can only conclude that you only deal with very trivial apps*, you conlude wrong. You seem to assume I use Application.ProcessMessages in my daily code. I don't and I haven't for years.
Lieven
@Lieven: So this: "for every application using ProcessMessages without problems I probably could retire" was not based on actual experience then. My mistake - I assumed you were making reference to empirical fact, not indulging in hyperbole. I apologise.
Deltics
@Deltics, no apologies needed. We'll just have to agree we don't agree ;)
Lieven
+1  A: 

One question to decide is whether or not your application can continue before you have the answer to the whatever the loop is calculating. If it cannot, then there is not much point in the application being "responsive". If you are trying to update a progress bar or something, you can call .Repaint on the control containing the progress bar every certain number of iterations to cause the progress bar to repaint.

If the application can continue, at least for a while, then putting the code in a thread is a good idea.

Putting the looping code in a thread is probably reasonable anyhow, especially if you want to do things like possibly abort the processing. If you have never used threads before, there is a bit of a learning curve, but for a simple loop like you describe there are lots of examples on the web.

Dave Novo
@Dave: I totally disagree. The application must always be responsive. If it is taking a while, the user may want to move it or minimize it. He can't if it's stuck in a loop.
lkessler
@lkessler: I agree
gbrandt
+2  A: 

I would also vote for a thread or something like Anreas' AsyncCalls. To prohibit the user doing any non-allowed actions during the time needed, you can set a flag when the routine starts and reset it when it ends (you have to update Screen.Cursor anyway). The main thread can check this flag and disable all affected actions in their OnUpdate event.

Uwe Raabe
Seems that mghie's comment came about the same time as my answer.
Uwe Raabe
+4  A: 

Put it under thread is not trival since it requires lock share resource if any. A good trick is having a counter, after processing # of loop, call ProcessMessages

var
  LoopCounter: Integer;

LoopCounter := 0;
R := FirstRecord;
while R <> nil do begin
  Inc(LoopCounter);
  if (LoopCounter >= ???) then
  begin
    LoopCounter := 0;
    Application.ProcessMessages;
  end;

  { do something simple with R.Value }
  R := R.NextRecord;
end;
APZ28
Putting it into a single secondary thread is completely trivial if the only other thread (the VCL thread) is kept from touching the same data structures *during that time*. "Shared resources" does not mean that more than one thread accesses the same data, it means that this access could potentially be concurrent.
mghie
+1 because this is often "good enough". Threads are better though.
Chris Thornton
+2  A: 

The best option is to move the loop into its own worker thread so the main thread is not blocked, then you do not need to call ProcessMessages() at all.

However, if you must do the loop in the main thread, then you can use MsgWaitForMultipleObject() to detect when to call ProcessMessages(), ie:

Screen.Cursor = crHourGlass; 
R := FirstRecord; 
while R <> nil do begin 
  { do something simple with R.Value } 
  if MsgWaitForMultipleObjects(0, nil, False, 0, QS_ALLINPUT) = WAIT_OBJECT_0 then
    Application.ProcessMessages;
  R := R.NextRecord; 
end; 
Screen.Cursor := crDefault; 

Alternatively with PeekMessage():

var Msg: TMsg;

Screen.Cursor = crHourGlass; 
R := FirstRecord; 
while R <> nil do begin 
  { do something simple with R.Value } 
  if PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) then
    Application.ProcessMessages;
  R := R.NextRecord; 
end; 
Screen.Cursor := crDefault; 

Alternatively with GetQueueStatus():

Screen.Cursor = crHourGlass; 
R := FirstRecord; 
while R <> nil do begin 
  { do something simple with R.Value } 
  if GetQueueStatus(QS_ALLINPUT) <> 0 then
    Application.ProcessMessages;
  R := R.NextRecord; 
end; 
Screen.Cursor := crDefault; 
Remy Lebeau - TeamB