How do I allow the user to cancel a time-consuming operation?

Ooooh, this question comes up ALL the time...

It frequently happens that you wish to carry out a time-consuming operation, but allow the user to abort that operation at any time. Also you may wish to display the progress of the operation using a common-control progress bar. The problem arises when you implement this, and carry out your operation in a tight loop. While that loop is running, your machine is not processing ("pumping") any messages, so your window doesn't receive WM_PAINT, preventing your progress bar from updating, and your user will be unable to push the Cancel/Abort button because you're not processing any WM_COMMAND messages.

There are a number of options open to you. Lets start with the ridiculous and work our way up to the sublime:

Option 1

Add a timer and do a chunk of processing on each timer tick. The upside is it's simple to implement. The downsides are:

  • You may well waste a lot of time after the processing is done, waiting for the next tick.
  • You have to organise it so if the next tick occurs before you've finished the last block of processing, your algorithm doesn't screw up.
  • The application won't scale well to a faster processor.
  • Windows timers are notoriously inaccurate.

I think you'll agree that this approach basically sucks.

Option 2

For MFC apps, perform a chunk of processing on each OnIdle call. MFC calls OnIdle when there aren't any more messages to fetch from the queue. Can be tricky to get right, and again it requires you to break the processing into small chunks, which may not suit your algorithm.

Option 3

Add a PeekMessage loop within your processing loop, calling it after each small chunk of processing. Here is a example of an MFC-friendly implementation of this technique:

while (<some criterion>)
{
   <do a small bit of processing>
    MFC_Yield ();
}
       .....

void  CMyClass::MFC_Yield ()
{
   MSG msg ;
      
   while (PeekMessage (&msg, 0, 0, 0, PM_NOREMOVE))
   {
      if (!(AfxGetApp()->PumpMessage()))
      {
         // regenerate WM_QUIT for main message loop.
         ::PostQuitMessage(0);
         break;
      }
   }
   // let MFC do its idle processing
   LONG lIdle = 0;
   while (AfxGetApp()->OnIdle(lIdle++));
}

This works well, but again it requires you to break the processing algorithm into chunks. It also gives rise to re-entrancy problems : you are in the middle of a function, then begin processing messages, any of which could cause your own WndProc to be called again, invoking some more of your code. Handling this re-entrancy can turn into a bit of a nightmare, as I found out many years ago when I wrote some comms code under Win16 which peeked left right and centre.

Option 4

Use a worker thread. Split the algorithm into a worker thread, and your main GUI thread can continue running as if the processing wasn't going on. The main thread can set a "please abort" event which the worker thread checks for. Again it's advisable to break the algorithm down so the thread can check its abort event every so often, but these breaks don't have to be as frequent as they do with the above solution because we don't have to worry about processing every last WM message that's flying around - just the latency between when the user presses Cancel and when the process actually stops. The worker can also post progress messages to the main thread window (or call its functions - they're in the same address space, remember).

If you MUST have an uninterrupted run, and you don't care about brutality, and the worker thread doesn't allocate any resource which might leak, then you could leave the algorithm as a straight loop and simply have the main thread call TerminateThread on the worker if the user cancels the operation. Beware though: doing this means any DLLs called by the worker won't receive a THREAD_DETACH, so you might get unexpected side-effects.

For details on implementing worker threads, see tip #31

Download