9.3. Performing Long Jobs

When performing long jobs, you want to

  • Let the user know that the application is working and the job is progressing.

  • Let the user access application functions that are still available.

To make either of these possible, you need to first work to keep the UI alive. This entails chopping our long job into small pieces and reentering the event loop after computing each small piece. The preferred way of accomplishing this is by responding to QTimer events.

9.3.1. Using QTimer to Perform Long Jobs

The QTimer class emits a signal every msec milliseconds in response to events it posts in the event queue. If you do all the work required for a long job in response to QTimer signals (that is, do our work in a slot connected to the QTimer::timeout() signal), this gives other events in the event queue, such as paint events, mouse press events, and so on, a chance to be processed. It is the processing of these events that will keep the UI alive.

An unacceptable alternative to this is to do all the work in a single method at once. The problem with this is that while the work is being performed, no events would be processed, and so the application's UI would "hang". The window would not update, mouse clicks would be ignored, and so on.

The code presented in Listings 9.4 and 9.5 show a widget called KlongJob, which demonstrates how to use QTimer to perform a long job. KLongJob flips a coin one million times, counts the number of times the coin comes up heads, and computes the percentage deviation from the ideal "50% heads". That is, the coin is expected to come up heads about half the time, but you know that it'll always be a little different than 50 percent until you've flipped the coin an infinite number of times. This calculation is ideal for this demonstration because it takes a long time but requires very little code.


Example 9.4. klongjob.h: Class Definition for KLongJob, a Main Widget That Demonstrates How to Use QTimer to Perform a Long Job

   1 
   2  1: #ifndef __KLONGJOB_H__
   3  2: #define __KLONGJOB_H__
   4  3:
   5  4: #include <ktmainwindow.h>
   6  5:
   7  6: class QTimer;
   8  7: class QLabel;
   9  8: class QPopupMenu;
  10  9:
  11 10: /**
  12 11:  * KLongJob
  13 12:  * Handle a long job while keeping the UI alive.
  14 13:  **/
  15 14:
  16 15: class KLongJob : public KTMainWindow
  17 16: {
  18 17:  Q_OBJECT
  19 18:
  20 19:  public:
  21 20:   KLongJob (const char *name=0);
  22 21:
  23 22:  private:
  24 23:   int count, total;
  25 24:   int idstart, idstop;
  26 25:   QTimer *qtimer;
  27 26:   QLabel *qlabel;
  28 27:   QPopupMenu *file;
  29 28:
  30 29:  private slots:
  31 30:
  32 31:    void slotStartComputation ();
  33 32:    void slotStopComputation ();
  34 33:
  35 34:   /**
  36 35:    * Do some of the calculation.
  37 36:    **/
  38 37:   void slotComputeSome ();
  39 38:
  40 39: };
  41 40:
  42 41: #endif
  43 

KLongJob is derived from KTMainWindow so that you can add a user interface to the program. You will see when you execute KLongJob how the user interface keeps working, even while the long calculation is being performed.


Example 9.5. klongjob.cpp: Class Declaration for KLongJob

   1 
   2  1: #include <qtimer.h>
   3  2:
   4  3: #include <kapp.h>
   5  4: #include <kaction.h>
   6  5: #include <kstdaction.h>
   7  6:
   8  7: #include "klongjob.moc"
   9  8:
  10  9: KLongJob::KLongJob (const char *name=0) :
  11 10:   KTMainWindow (name)
  12 11: {
  13 12:   start = 
  14 13:     new KAction ("&Start", 0,  this, SLOT(slotStartComputation()), 
  15 14:          actionCollection(), "start");
  16 15:   stop =
  17 16:     new KAction ("Sto&p", 0,  this, SLOT(slotStopComputation()), 
  18 17:          actionCollection(), "stop");
  19 18:   KStdAction::quit (kapp, SLOT (closeAllWindows()),
  20 19:             actionCollection());
  21 20:   stop->setEnabled (false);
  22 21:
  23 22:   createGUI();
  24 23:
  25 24:   qlabel = new QLabel (this);
  26 25:   qlabel->setAlignment (QLabel::AlignCenter);
  27 26:   setView (qlabel);
  28 27:
  29 28:   qtimer = new QTimer (this);
  30 29:   connect ( qtimer, SIGNAL (timeout()),
  31 30:        this, SLOT (slotComputeSome()) );
  32 31:
  33 32: }
  34 33:
  35 34: void
  36 35: KLongJob::slotStartComputation ()
  37 36: {
  38 37:   start->setEnabled (false);
  39 38:   stop->setEnabled (true);
  40 39:
  41 40:   qtimer->start (0);
  42 41:
  43 42:   count=total=0;
  44 43: }
  45 44:
  46 45: void
  47 46: KLongJob::slotStopComputation ()
  48 47: {
  49 48:   start->setEnabled (true);
  50 49:   stop->setEnabled (false);
  51 50:
  52 51:   qtimer->stop();
  53 52: }
  54 53:
  55 54: void
  56 55: KLongJob::slotComputeSome()
  57 56: {
  58 57:   const int NumberOfFlips = 10;
  59 58:   double deviation;
  60 59:   int i;
  61 60:
  62 61:   for (i=0; i<NumberOfFlips; i++)
  63 62:     if (kapp->random()%2==1)
  64 63:       count++;
  65 64:   total+=NumberOfFlips;
  66 65:
  67 66:   if (!(total%5000))
  68 67:     {
  69 68:       deviation = (count - total/2.)/(double)total;
  70 69:       QString qstring;
  71 70:       qstring.sprintf ("Total flips: %10d\nDeviation from 50%% heads: %10.5f",
  72 71:                total, deviation);
  73 72:       qlabel->setText (qstring);
  74 73:     }
  75 74:
  76 75:   if (total>=1000000)
  77 76:     slotStopComputation();
  78 77: }
  79 

In the KLongJob constructor, shown in Listing 9.5, you create a QTimer and connect its timeout signal to our slot slotComputeSome() (lines 28–30). The slot performs some of the computation every time the QTimer times out.

The File menu entry, Start, is connected to the slot slotStartComputation() (lines 12–14). In this slot you begin the computation by calling


   1 
   2 qtimer->start(0)
   3 

This statement starts the QTimer with a timeout of 0 milliseconds. Using a value of zero here means that the timeout() signal will be emitted as soon as all events in the queue have been processed. In other words, a timeout event is appended to the event queue and processed in turn by Qt. If nothing is happening with our UI—that is, no events are being posted—when slotcomputeSome() exits, it is reentered right away with little time lost.

The slot slotcomputeSome() executes 10 coin flips (more precisely, it chooses randomly between 0 and 1 10 times) in lines 61–63. The classwide variables, total and count, are used to save the state of the computation between calls to slotcomputeSome(). After every 5,000 flips (50 calls to slotcomputeSome()) the display is updated. I chose not to update the display after every call to slotcomputeSome() because updates are slow and can add a lot of time to the computation. It is important to update the display often enough to keep the user informed that things are proceeding as planned, but not so often as to add significant time to the task being performed.

It is important to realize that the user would never see the progress indicator you have created—the "Total flips" and "Deviation from 50% heads" messages—if you didn't return to the event queue to allow the paint events to be processed. (Whenever you change the text of a QLabel, it sends itself a paint event.)

Finally, after flipping the coin one million times, line 76 calls stopComputation(). In this method, you call


   1 
   2 qtimer->stop()
   3 

which stops qtimer from posting any more timeout events.

You should give KLongJob a try. Start the computation by choosing File, Start, and notice that you can resize the window, drag other windows over it, and even close the window while the computation is being performed.

The following main() function in Listing 9.6 can be used to create an execute KLongJob. You will also need to place the file klongjobui.rc (available on this book's Web site) in the directory $KDEDIR/share/klongjob. You can see a screen shot of KLongJob in Figure 9.2.


Example 9.6. main.cpp: A main() Function Suitable for Testing KLongJob

   1 
   2  1: #include <kapp.h>
   3  2:
   4  3: #include "klongjob.h"
   5  4:
   6  5: int
   7  6: main (int argc, char *argv[])
   8  7: {
   9  8:   KApplication kapplication (argc, argv, "klongjobtest");
  10  9:   KLongJob *klongjob = new KLongJob (0);
  11 10:
  12 11:   kapplication.setMainWidget (klongjob);
  13 12:
  14 13:   klongjob->show();
  15 14:   return kapplication.exec();
  16 15: }
  17 

9.3.2. Enabling/Disabling Application Functions

While trying out Klongjob you may have noticed that the Start menu entry is grayed out when the program is flipping the coin (see line 37).

It is important to disable the UI controls that give the user access to the long job if it does not make sense to start the job again. An electronic mail client, for example, should disable its Check for New Mail buttons and menu entries while it is checking for new mail (but keep the rest of its UI alive so that the user can read messages), but a Web browser does not need to disable any hyperlinks while it is attempting to connect to a remote site to download a page. If the user clicks another hyperlink while waiting, a browser, generally, cancels the pending request and starts fulfilling the new one.


Figure 9.2. Klongjob performs a long calculation while still allowing user interaction.


On line 38, in the method startComputation(), you have enabled the File menu entry Stop. This enables the user to cancel the long job.

This is an important function to offer the user. The user may have accidentally chosen to start the job or simply decided the results weren't worth waiting for. In any event, the user should decide what the CPU cycles are being spent on.

9.3.3. Speed Issues

Clearly, performing a long computational task in the way just presented takes longer than performing it all in one method without checking the event queue, but the extra time should be considered well spent for the various reasons given previously.

When deciding how much work to do in the computeSome() method, consider two competing factors:

  • Efficiency

  • Smoothness of user interaction

Efficiency requires more work to be done in each call to computeSome(), which means that a higher percentage of the overall time is spent working on the job, and thus, overall time is decreased. Smoothness of user interaction requires less time to be spent working on the job, and thus, overall time is increased. You set the amount of work small enough so that user interaction did not suffer noticeably.

9.3.4. An Alternative to QTimer

There is another way to process events while performing a long job. A method in the class QApplication (from which KApplication is derived), called processEvents(), processes all the pending events and then returns.

Using this method, write the code for the long job in one big loop and call processEvents() occasionally. See Listing 9.7 for a second version of klongjob, which uses processEvents().


Example 9.7. Modified Version of KlongJob, Which Uses QApplication::processEvents()

   1 
   2  1: #include <kapp.h>
   3  2: #include <kaction.h>
   4  3: #include <kstdaction.h>
   5  4:
   6  5: #include "klongjob.moc"
   7  6:
   8  7: KLongJob::KLongJob (const char *name=0) :
   9  8:   KTMainWindow (name)
  10  9: {
  11 10:   start = 
  12 11:     new KAction ("&Start", 0,  this, SLOT(slotCompute()),
  13 12:          actionCollection(), "start");
  14 13:   stop =
  15 14:     new KAction ("Sto&p", 0,  this, SLOT(slotStopComputation()),
  16 15:          actionCollection(), "stop");
  17 16:
  18 17:   KStdAction::quit (kapp, SLOT(closeAllWindows()),
  19 18:             actionCollection());
  20 19:
  21 20:   createGUI();
  22 21:
  23 22:   stop->setEnabled (false);
  24 23:
  25 24:   qlabel = new QLabel (this);
  26 25:   qlabel->setAlignment (QLabel::AlignCenter);
  27 26:   setView (qlabel);
  28 27: }
  29 28:
  30 29: void
  31 30: KLongJob::slotStopComputation()
  32 31: {
  33 32:   bcontinuecomputation=false;
  34 33: }
  35 34:
  36 35: void
  37 36: KLongJob::slotCompute()
  38 37: {
  39 38:   double deviation;
  40 39:   int i;
  41 40:   count=total=0;
  42 41:
  43 42:   bcontinuecomputation=true;
  44 43:
  45 44:   start->setEnabled (false);
  46 45:   stop->setEnabled (true);
  47 46:
  48 47:   kapp->processEvents();
  49 48:
  50 49:   for (i=0; i<1000000 &&bcontinuecomputation; i++)
  51 50:     {
  52 51:       if (kapp->random()%2==1)
  53 52:     count++;
  54 53:       total++;
  55 54:
  56 55:       if (!(total%100))
  57 56:     kapp->processEvents();
  58 57:
  59 58:
  60 59:       if (!(total%5000))
  61 60:     {
  62 61:       deviation = (count - total/2.)/(double)total;
  63 62:       QString qstring;
  64 63:       qstring.
  65 64:         sprintf ("Total flips: %10d\nDeviation from 50%% heads: %10.5f",
  66 65:              total, deviation);
  67 66:       qlabel->setText (qstring);
  68 67:     }
  69 68:     }
  70 69:
  71 70:   start->setEnabled (true);
  72 71:   stop->setEnabled (false);
  73 72:
  74 73: }
  75 

This version of KLongJob looks essentially the same to the user as the previous version, but the programming style is quite different. The slot slotCompute() does all the work in one loop.

Every so often (after flipping the coin 100 times) in slotCompute(), you call (line 56)


   1 
   2      kapp->processEvents();

which allows the paint event that is generated by qlabel when you change its text (line 66) to be processed, as well as any user input or other events that have been posted.

I don't recommend using processEvents() for long jobs, although you will find it used this way occasionally. The problem with it is that some events can't properly be processed if they need to eventually return control to the method that called processEvents(). As an example, run klongjob, choose File, Start, and then press Ctrl+Q before the calculation finishes. You should get this message


   1 
   2 Segmentation fault (core dumped)
   3 

(or something similar). This has happened because your request to terminate the program—which included deleting the current instance of KLongJob—was processed, and then an attempt to return control to the method slotCompute(), part of the deleted instance of KLongJob, was made. This problem could be circumvented, but the possibility still exists that, in a more complex program, you could run into other, similar problems. A safer and more elegant design uses the QTimer method described previously.

You can use the main() function given in Listing 9.6 to try this program. Its UI looks the same as Figure 9.2.