Walletfox.com

How to report progress and abort a long-running operation with QRunnable and QThreadPool


This article shows prime number validation that runs in a separate thread. Prime number validation is one example of a long operation which needs to be put into a separate thread in order not to freeze the graphical user interface. In this article, I am also going to explain how to report the progress of the calculation and how to abort it.

Introduction - basic background on threads

This section provides a basic background on threads without going into synchronization. You can skip this part if you are already familiar with threads. The main difference between a process and a thread is that each process is executed in a separate address space, whereas each thread uses the same address space of a process. A process can have multiple threads. Threads share the heap, but each thread of execution has its own call stack.

In our example, we will use a Qt's QThreadPool object to manage a collection of threads. The QThreadPool object of an application can be accessed by calling QThreadPool::globalInstance(). To be able to use one of the QThreadPool threads, we have to subclass QRunnable and implement the run() method. After that, we have to create an instance of the QRunnable subclass and pass it to QThreadPool::start(). This is shown in the code snippet below. The figure explains the stack allocations for different stages of code execution.

#include <QThreadPool>
#include <QDebug>
#include <QApplication>

class MyRunnable : public QRunnable
{
public:
    MyRunnable():QRunnable(){}
    void run(){
        for(int i = 0; i < 3; i++)
            qDebug() << "i " << i;
    }
};

void print(){
    QRunnable* printRunnable = new MyRunnable();
    QThreadPool::globalInstance()->start(printRunnable);
}

int main(int argc, char *argv[]){
    QApplication a(argc, argv);
    print();
    return 0;
}

The program produces the output below. The print is being executed in a separate thread.

i 0
i 1
i 2

Little is guaranteed when it comes to threads. The only guarantee with threads is that each thread will start and each thread will run to completion. Within each thread, code executes in a predictable order. However, actions of different threads can mix in an unpredictable manner. This can be seen below.

#include <QThreadPool>
#include <QDebug>
#include <QApplication>

class MyRunnable : public QRunnable
{
public:
    MyRunnable():QRunnable(){}
    void run(){
        for(int i = 0; i < 3; i++)
            qDebug() << QThread::currentThreadId() << " i " << i;
    }
};

int main(int argc, char *argv[]){
    QApplication a(argc, argv);

    QThreadPool::globalInstance()->start(new MyRunnable);
    QThreadPool::globalInstance()->start(new MyRunnable);
    QThreadPool::globalInstance()->start(new MyRunnable);

    return 0;
}

The program produces the output below (you might see a slightly different output on your computer). Notice how a particular thread (e.g. 0x32c0) executes in a predictable order (0,1,2) but the ordering among the threads is rather unpredictable.

This section has been adapted from Sun Certified Programmer for Java 6 Exam 310-065 (2008) by Kathy Sierra and Bert Bates.

How to place prime number validation into a separate thread

Firstly, let's quickly summarize the definition of a prime number. A prime number is a natural number greater than 1 that is divisible only by 1 and itself. 0 and 1 are not prime numbers since they do not fulfill this criterion. Here you can see a couple of prime numbers which you can enter into our application:

To implement the prime number validation, together with the possibility of a progress report and abort we need to do the following:

  • To perform the validation in a separate thread we need to subclass QRunnable, implement its run() method and use it with a QThreadPool object.
  • To indicate abort or termination of the calculation we need to introduce a boolean 'stopped', stored in the prime widget and referenced by a QRunnable instance.
  • To report the progress of the calculation from within the secondary thread we use QMetaObject::invokeMethod().

How to subclass QRunnable

The prime number validation is calculated by an instance of the class PrimeRunnable subclassed from QRunnable. The actual code for the validation of the number has to be put into the run() method. The run() was made private to prevent it from being called directly on instances of PrimeRunnable (rather than via QThreadPool::globalInstance()->start()).

Notice that we store a couple of attributes within the PrimeRunnable class. We store a pointer to the main widget so that we have a way of reporting progress. We also store the pointer to the boolean responsible for aborting the calculation (this variable will be changed from outside - i.e. by the main GUI thread). Next, we store the candidate for the prime number n, i.e. the number entered by the user. Last, we store the result of the calculation in m_prime. We also define a const int pIncrement = 1 (this means that we will be reporting progress by 1%, a value of 2 would mean that we report progress by 2%, etc.).

If you look into the public section of the class, you will see that the result can be retrieved by isPrime(). As you will shortly see in the source code of the PrimeWidget, this presumes that we disable auto-deletion of the runnable by the QThreadPool.

const int pIncrement = 1;

class PrimeRunnable : public QRunnable
{
public:
    PrimeRunnable(QWidget* receiver, volatile bool *stopped,
                  qulonglong n);
    bool isPrime() const {return m_prime;}

private:
    void run();
    qulonglong n;
    QWidget* receiver;
    volatile bool *m_stopped;
    bool m_prime;
};

The run() method naively checks whether the number is a prime or not. For this, it uses trialFactors starting from 2 until the square root of the number entered by the user. If the calculation has been canceled by the user, the method does not finish the loop and returns. We report the progress by calling QMetaObject::invokeMethod(). We do not report the progress for every trialFactor but rather in 1% intervals (valueIncrement stores the trialFactor that corresponds to a particular integer percentage). If the trialFactor divides the number entered by the user, the number is not prime, as a result, we do not change the default boolean value and return from the method. If the check finishes and no divisor is found, we set the m_prime to true. If the user enters 0 or 1 the method returns as these numbers are not considered to be prime.

void PrimeRunnable::run() {
    if(n == 0 || n == 1)
        return;
    qulonglong sRoot = sqrt(n);
    qulonglong valueIncrement = pIncrement*sRoot/100;
    for (qulonglong trialFactor = 2; trialFactor <= sRoot; trialFactor++) {
        if(*m_stopped)
            return;
        if(valueIncrement >=1 && trialFactor % valueIncrement == 0){
            int pFinished = trialFactor*100 / sRoot;
            QMetaObject::invokeMethod(receiver, "updateProgressBar",
                                      Qt::QueuedConnection,
                                      Q_ARG(int, pFinished));
        }
        if(n % trialFactor == 0)
            return;
    }
    m_prime = true;
}

The progress of the execution in the secondary thread is reported via QMetaObject::invokeMethod() invoking PrimeWidget::updateProgressBar().

void PrimeWidget::updateProgressBar(int progressPercent){
    if(stopped)
        return;
    this->progressBar->setValue(progressPercent);
}

Implementation of the PrimeWidget

Below you can see the header of a subclass of a QWidget called PrimeWidget. The PrimeWidget stores a pointer to our runnable (called primeTask) and a boolean 'stopped' that signalizes whether the calculation is in progress or has been aborted or finished.

The class also contains a couple of important slots, namely:

  • updateProgressBar() is invoked in regular intervals by an instance of PrimeRunnable via QMetaObject::invokeMethod()
  • calculateIfPrime() instantiates a new PrimeRunnable and starts it from the QThreadPool object
  • abort() aborts the calculation
  • updateChildWidgets() enables or disables the child widgets based on whether the calculation started, has been cancelled, produced a result etc.
  • checkIfDone() checks recursively in regular intervals whether the thread executing the prime number calculation is still active
class PrimeWidget : public QWidget
{
    Q_OBJECT

public:
    explicit PrimeWidget(QWidget *parent = 0);
    ~PrimeWidget();

private slots:
    void updateProgressBar(int progressPercent);
    void calculateIfPrime();
    void abort();
    void updateChildWidgets();
    void checkIfDone();

protected:
    void closeEvent(QCloseEvent *);

private:
    QLabel* numberLabel;
    QLabel* resultLabel;
    QLineEdit* numberEdit;
    QPushButton* calculateButton;
    QPushButton* abortButton;
    QProgressBar* progressBar;

    volatile bool stopped;
    PrimeRunnable* primeTask;
};

In the PrimeWidget's constructor we initiate and enable/disable child widgets, i.e.just after we open the application, the "Check if Prime' button should be enabled, on the other hand, the 'Abort' button should be disabled. The notable part of the constructor is the progressBar, which we make invisible by default and only make it visible once we start the prime number validation. Notice the connections: we connect the calculateButton's signal clicked() to the slot calculateIfPrime() and the abortButton's signal clicked() to the slot abort(). Last, notice that I used the initialization list to initialize the pointer to the primeTask runnable.

PrimeWidget::PrimeWidget(QWidget *parent) :
    QWidget(parent), primeTask(0)
{
    setWindowTitle("Prime number validator");
    QVBoxLayout* mainLayout = new QVBoxLayout(this);
    QHBoxLayout* inputLayout = new QHBoxLayout;
    QHBoxLayout* buttonLayout = new QHBoxLayout;

    numberLabel = new QLabel(tr("Enter a number:"));
    numberEdit = new QLineEdit;

    inputLayout->addWidget(numberLabel);
    inputLayout->addWidget(numberEdit);

    mainLayout->addLayout(inputLayout);

    calculateButton = new QPushButton(tr("Check if prime"));
    abortButton = new QPushButton(tr("Abort"));
    abortButton->setEnabled(false);
    buttonLayout->addWidget(calculateButton);
    buttonLayout->addWidget(abortButton);
    mainLayout->addLayout(buttonLayout);

    resultLabel = new QLabel(tr("Prime?"));
    QFont f("Arial", 10, QFont::Bold);
    resultLabel->setFont(f);
    resultLabel->setAlignment(Qt::AlignCenter);
    resultLabel->setTextFormat(Qt::RichText);
    mainLayout->addWidget(resultLabel);

    progressBar = new QProgressBar;
    mainLayout->addWidget(progressBar);
    progressBar->setVisible(false);

    QObject::connect(calculateButton, SIGNAL(clicked()),
                     this, SLOT(calculateIfPrime()));
    QObject::connect(abortButton, SIGNAL(clicked()),
                     this, SLOT(abort()));
}

The slot calculateIfPrime() is called whenever we press the calculateButton. The method checks the validity of the number entered by the user. It also adjusts the user interface, creates an instance of the PrimeRunnable, disables its auto-deletion (so that we can retrieve its result by calling PrimeRunnable::isPrime()) and starts the task via QThreadPool object. Last, it calls checkIfDone().

void PrimeWidget::calculateIfPrime(){
    bool numberCorrect = false;
    qlonglong n = numberEdit->text().toULongLong(&numberCorrect);
    if(numberCorrect){
        stopped = false;
        updateChildWidgets();
        primeTask = new PrimeRunnable(this, &stopped, n);
        primeTask->setAutoDelete(false);
        QThreadPool::globalInstance()->start(primeTask);
        checkIfDone();
    }
    else
        resultLabel->setText("<font color = '#CC0000'>"
                     "You did not enter an usigned 64 bit integer.</font>");
}

checkIfDone() checks recursively in regular intervals (100 ms) whether the thread executing the prime number validation is still active. If not, the boolean 'stopped' is set to true and the user interface is updated based on the result of the calculation. After retrieving the result, we delete the PrimeRunnable instance and reset the pointer of primeTask to 0 (this is necessary, because we create an instance of PrimeRunnable every time the user clicks on the "Check if prime' button, furthermore, we disabled the deletion of the runnable by the QThreadPool object).

void PrimeWidget::checkIfDone(){
    if(QThreadPool::globalInstance()->activeThreadCount())
        QTimer::singleShot(100, this, SLOT(checkIfDone()));
    else {
        if(!stopped) {
            stopped = true;
            updateChildWidgets();
            if(primeTask->isPrime())
                resultLabel->setText(
                "<font color = '#003582'>The number is prime.</font>");
            else
                resultLabel->setText(
                "<font color = '#CC0000'>The number is not prime.</font>");
        }
        if(primeTask){
            delete primeTask;
            primeTask = 0;
        }
    }
}

If the user aborts the calculation, we set the boolean 'stopped' to true to abort the calculation as soon as possible (we do not want to wait until the secondary thread checks all the numbers if the calculation has been aborted). If the secondary thread still happens to be active in the thread pool, we waitForDone(), i.e. we wait for the secondary thread to exit. After that, we enable/disable the child widgets and update the text of the resultLabel. Similarly to checkIfDone() we delete the instance of PrimeRunnable.

void PrimeWidget::abort(){
    stopped = true;
    if (QThreadPool::globalInstance()->activeThreadCount())
        QThreadPool::globalInstance()->waitForDone();
    updateChildWidgets();
    resultLabel->setText(
    "<font color = '#CC0000'>Calculation aborted.</font>");
    if(primeTask){
        delete primeTask;
        primeTask = 0;
    }
}

Last, we reimplement the closeEvent() method. Before terminating the application we set the boolean 'stopped' to true and wait for all secondary threads to exit. At the end, we call event->accept().

void PrimeWidget::closeEvent(QCloseEvent *event){
    stopped = true;
    if(QThreadPool::globalInstance()->activeThreadCount())
        QThreadPool::globalInstance()->waitForDone();
    event->accept();
}

That's it! You can try out the prime number validation.

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    PrimeWidget w;
    w.show();
    return a.exec();
}