Tutorial: Writing Multi Threaded Application in Qt

in qt

Like most GUI frameworks, event handling in Qt by default happens on the main thread. This works well for small UI related changes, but breaks when we have to perform more work.

In this tutorial we'll write an image converter application that uses background worker thread to perform CPU intensive operation while keeping the GUI responsive. We'll also discuss processEvents and its advantages and disadvantages comapred with background thread.

A Single Threaded Image Converter

Our simple image converter will allow the user to select image files and then use Qt's QImage to change their resolution and add a small banner at the bottom of each image. The code to handle each image is as follows:

void ImageConverter::convert(QStringList imageFiles)
{
    for (QString fileName : imageFiles)
    {
        QImage src(fileName);
        QSize outSize(640, 480);
        QImage result = src.scaled(outSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
        QPainter painter(&result);
        QColor bgColor(0, 0, 0, 150);
        painter.fillRect(0, 350, 640, 130, bgColor);
        painter.setPen(Qt::white);
        painter.setFont(QFont("serif", 24, 500));
        painter.drawText(10, 400, "Made With 💚");

        QString outFileName = fileName.left(fileName.length() - 4) + "-edited.png";

        result.save(outFileName, "PNG");
        emit convertDone(fileName);

    }
}

I've build a small GUI around this code that looks something like this:

Screen-Shot-2017-10-13-at-17.08.38-

When a user clicks the "Convert" button the following code is called:

void MainWindow::convert()
{
    QStringList fileNames;

    QList<QListWidgetItem *> items = ui->listWidget->findItems("", Qt::MatchContains);
    for (auto i : items)
    {
        fileNames.push_back(i->text());
    }
    m_converter->convert(fileNames);
}

And to handle convertDone signal which happens after each file is converted we have the following function:

void MainWindow::handleConvertDone(QString fileName)
{
    auto items = ui->listWidget->findItems(fileName, Qt::MatchExactly);
    for (auto item : items)
    {
        item->setText("done! ... " + item->text());
    }
}

You can find the full source code for this project on Github at:
https://github.com/ynonp/qt-multithread-demo/tree/master/before

But when we compile and run the application something doesn't feel right. The images are converted one after the other without any indication on the GUI. In addition the GUI is completely frozen during conversion. Only after the loop in ImageConverter would end we'll be able to continue using the app.

A second problem arises in handleConvertDone function. This function should have added "Done" to each file after it was converted, but it is not called until after all files are converted.

In real world freezing GUIs like this are not acceptable and will cause your users to believe something is wrong or stuck.

Solution 1: Process Events

One easy workaround Qt offers is to call qApp->processEvents. Qt's event loop keeps on running and queueing events even as our main thread is busy. Calling processEvents yields a small pause in our work and lets Qt empty queued items. This will allow some interactivity in the program without adding more threads.

To use processEvents we can call it after each file conversion for example in the slot that handles convertDone:

void MainWindow::handleConvertDone(QString fileName)
{
    auto items = ui->listWidget->findItems(fileName, Qt::MatchExactly);
    for (auto item : items)
    {
        item->setText("done! ... " + item->text());
        // partial fix: process events after each finished item
        qApp->processEvents();
    }
}

With this solution in place the application will take a moment after each photo conversion to handle all the events queued while the conversion took place. Since image conversion is relatively quick, this will give our users a much better feeling. This fix will also allow a user to see "Done" added to each item after it was converted.

But processEvents is rarely a good solution for this kind of problems. Most background operations can't be easily divided to small chunks of work, and even if they can being non-responsive during the times between chunks is usually not acceptable.

Solution 2: Adding Worker Thread

A better solution would be to add a worker thread that will perform the image conversion in the background, and will notify our main thread when each photo conversion was done.

Qt has a simple pattern for such worker threads that are responsible for a single task and will report back progress.

Each QObject has a thread affinity, which means that QObject is connected to a specific thread. Whenever a QObject is required to handle a signal, the signal handler will be executed in the thread connected with this QObject.

By default all our QObjects are connected to the main GUI thread, but it's easy to move them around to other worker threads by calling QObject::moveToThread.

To add a worker thread we'll:

  1. Create a new QThread in main and "move" our image converter to that new thread.
  2. Change how we communicate with the image converter: Instead of directly calling its methods we'll use Qt's signals/slots mechanism. Qt's signals/slots check the thread affinity of the target QObject and executes the signal handler in the connected thread.

In some cases you'll also need to add a new signal to notify your GUI thread that work's done.

The modifications in code are therefore:

In file main.cpp:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    ImageConverter converter;
    QThread t;
    converter.moveToThread(&t);
    t.start();

    MainWindow w(converter);
    w.show();

    return a.exec();
}

In file mainwindow.cpp:

void MainWindow::convert()
{
    QStringList fileNames;

    QList<QListWidgetItem *> items = ui->listWidget->findItems("", Qt::MatchContains);
    for (auto i : items)
    {
        fileNames.push_back(i->text());
    }
    ui->progressBar->setMaximum(fileNames.size());
    QMetaObject::invokeMethod(
                &m_converter,
                "convert",
                Q_ARG(QStringList, fileNames));
}

Note the last line in the new convert function. Using QMetaObject::invokeMethod tells Qt to call the convert method through its signals/slots system taking into account the converter's thread affinity. Had we called the function directly it would have executed in the same thread, which is not what we wanted.

Alternatively, we could have defined a new signal in our MainWindow class and have ImageConverter listen to that signal.

The full source code for the fixed version is available on github:
https://github.com/ynonp/qt-multithread-demo/tree/master/after

Comments