הלינקייה: מגזין חודשי למפתחים

רוצה לשמוע על כל האירועים, המדריכים, הקורסים והמאמרים שנכתבו החודש ?
הלינקייה הינו מגזין חופשי בעברית שמשאיר אותך בעניינים.
בלי ספאם. בלי שטויות. פעם בחודש אצלך בתיבה.

Qt Multi Threaded Programming

Qt multi threads from Ynon Perek

The concept behind multi threaded programming is easy to grasp - instead of having a program do "just one thing", let's have multiple parallel threads of execution, and then our program will perform multiple tasks at the same time. Since the operating system is smart enough to "fake" parallelism when there's not enough cores, we're ok. Well, not exactly.

In real life, writing code that does multiple tasks in parallel introduces new problems and bugs rising from synchronization issues between the tasks. In this article I'll explore what Qt has to offer to developers writing concurrent applications, and consider the best mechanism to use for every situation. Although writing parallel applications can be a pain, Qt does a lot to ease that pain.

Qt Thread Pool

QThreadPool is a mechanism to use threads without worrying. Essentially, it lets you define your tasks and send them all to a thread pool that'll dynamically decide how many threads it's best to create, run all tasks in parallel and let you know when they're done. It's most suitable for algorithm that need to act on large inputs by dividing it to smaller tasks.

To use the thread pool, we need to define a class that derives from QRunnable and implement its run method. A QThreadPool maintains QRunnable objects and executes them. There's already a singleton thread pool that Qt manages for us, so you can add tasks from within any subroutine.

For example, the following runnable increases each list item by 1:

class IncTask : public QRunnable
{
public:

    IncTask( QList<int> & numbers, int start, int end ):
        m_numbers ( numbers ),
        m_start   ( start   ),
        m_end     ( end     )
    {
    }

    virtual void run()
    {
        QList<int>::iterator i;
        for ( i = m_numbers.begin() + m_start; i < m_numbers.begin() + m_end; ++i )
        {
            *i += 1;
        }
    }

private:
    QList<int> & m_numbers;
    int m_start, m_end;
};

Note that since run() is automatically called by the thread pool, it's impossible to pass arguments to that method, so we use pass all the data in the constructor. Here's how we might use that class:


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

    QList<int> numbers;
    numbers << 10 << 20 << 30 << 40 << 50 << 60 << 70 << 80;

    IncTask *t1 = new IncTask( numbers, 0, 4 );
    IncTask *t2 = new IncTask( numbers, 4, 6 );

    QThreadPool::globalInstance()->start(t1);
    QThreadPool::globalInstance()->start(t2);
    QThreadPool::globalInstance()->waitForDone();

    qDebug() << numbers;
    return 0;
}

Note: A QThreadPool takes ownership on every task you give it, and will delete it after run() ends. If you need to manage the memory yourself, call setAutoDelete(false) on the task object before submitting it to the thread pool.

QtConcurrent Algorithms and QFuture

The second option for writing concurrent applications is to use one of the 3 stock Qt algorithms to work on large bulks of data:

Method Description
filtered() Iterate over a sequence and returns only the elements that pass a condition
mapped() Iterate over a sequence using a mapping function, and returns a new list of the results
filteredReduced() same as filtered(), but also take a function and use it to reduce the values.
Reduction is performed by running a reduction function on every pair of values, for example
if values are: x1, x2 ,x3, x4 and reduction function f - reduce is: f( f( f( x1, x2 ), x3 ), x4 ).
QtConcurrent algorithms make it easy to watch progress using a QFutureWatcher. Here's how we might use filteredReduce() to calculate the sum of even integers from 0 to 100:

void add( int &resultSoFar, const int &newValue )
{
    resultSoFar += newValue;
}

bool isEven( const int & x )
{
    return x % 2 == 0;
}

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

    QList<int> numbers;
    for ( int i=0; i <= 100; i += 1 ) numbers << i;

    QFuture<int> futureSum = QtConcurrent::filteredReduced( numbers, isEven, add );

    futureSum.waitForFinished();
    qDebug() << futureSum.result();

    return 0;
}

As stated earlier, these functions can also report progress using QFutureWatcher. Just create a QFutureWatcher on the QFuture object you got from map/reduce functions, and connect to its signals. In code, this looks like this:

QFutureWatcher<int>sumWatcher;
sumWatcher.setFuture( futureSum );

// resultReadyAt() signal is emitted when a single result is ready
QObject::connect( &sumWatcher, SIGNAL(resultReadyAt(int)), myObj, SLOT(resultReady(int)));

// finished() signal is emitted when entire task is done
QObject::connect( &sumWatcher, SIGNAL(finished()), myObj, SLOT(missionComplete()));

Background Worker Threads

Another common use for threads is having a background worker that performs long tasks. This is especially useful in GUI applications, because we only have one UI thread, and if one of your event handlers takes to long to return, the entire application "freezes" until that handler finishes its work.

To work around that, we use background workers. These are threads run on a different event loop. We communicate with a background worker by emitting a signal, and we get progress notifications by implementing slots.

Examine the following code:

#include <QtGui/QtGui>
#include "backgroundworker.h"

int main(int argc, char **argv)
{
    QApplication app( argc, argv );

    BackgroundWorker w;

    QPushButton b("Push Me");
    QObject::connect( &b, SIGNAL(clicked()), &w, SLOT(doSomething()) );

    b.show();

    app.exec();
}

If BackgroundWorker::doSomething is a long action, pressing the button freezes your application until the action is finished. A user cannot move the window, interact with other widgets or even leave the app. Her only option is to force quit using the operating system.
Moving the background worker to a different thread is amazingly simple using Qt's QThread. Here's how it looks like:


#include <QtGui/QtGui>
#include "backgroundworker.h"

int main(int argc, char **argv)
{
    QApplication app( argc, argv );

    QThread t;
    t.start();

    BackgroundWorker w;
    w.moveToThread( &t );

    QPushButton b("Push Me");
    QObject::connect( &b, SIGNAL(clicked()), &w, SLOT(doSomething()) );

    b.show();

    app.exec();

    t.exit();
}

Here's what we did:

1. Create a new QThread and called its start() method.

2. Called moveToThread() method on our object to move it to the new thread's event loop.

3. In the end, we call t.exit() to close everything nicely.

Note: Keep in mind when using background workers, that if an object has a parent, it cannot be moved, so if you need an object hierarchy call moveToThread() on the top level object.

It's also good to keep in mind that there's only one thread executing your background worker task, so let's say a user on the previous example pressed the button 3 times, doSomething() method would get called 3 times serially (one after the other).

Using background workers is best suited for cases where you have a handler method that takes a long time to run, needs to run occasionally without stopping the program, and the worker always remains in memory.

Multi Threaded Takeaways

As applications get bigger, the need to take some functionality and perform it in the background is unavoidable. Qt provides us with the mechanisms to execute background jobs without disrupting our main application. The best thing about it, is that it scales up. You can start writing your application logic single-threaded using Qt's signals and slots. As you discover slow spots in the application, you can easily take that and move it to a background worker, or speed up the algorithm using a thread pool and map/reduce.

Qt also provides the mechanisms to synchronize threads, supporting QMutex, QSemaphore, QReadWriteLock and QWaitCondition. Although not covered in this article, their behavior corresponds to the least surprise principle and if you've worked with them in other languages you should have an easy time setting it up in Qt.

Don't forget to share your multi threaded Qt code experiences in the comments below.