Keeping the GUI Responsive

Материал из Wiki.crossplatform.ru

Версия от 01:51, 20 февраля 2009; Lit-uriy (Обсуждение | вклад)
(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 27 | Документация


by Witold Wysota

На сайт QtCentre люди приходят с повторяющейся проблемой, их GUI перестает отвечать во время выполнения длительных операций. Эту проблему не трудно преодолеть, но вы можете сделать это по-разному, поэтому я хотел бы представить ряд возможных вариантов, которые могут быть использованы в зависимости от ситуации.

Содержание


[править] Выполнение длительных операций

Первое, что нужно сделать, это определить области проблемы и наметить пути для ее решения. Указанные проблемы могут принимать одну из двух форм. Первая, когда программа должна выполнить задачу, которая описывается как ряд операций, которые будут выполняться последовательно, с тем чтобы получить конечный результат. Примером такой задачи вычисления быстрого преобразования Фурье.

Второй вариант - это когда программа должна запустить какое-то действие (например, загрузку из сети) и дождаться её окончания перед тем, как перейти к следующему шагу алгоритма. Этот вид проблемы как таковую легко избежать используя Qt, потому что большинство асинхронных задач выполняется следующим образом: фреймворк посылает сигнал, когда задача выполнена, а вы можете привязать его к слоту, который продолжит алгоритм.

Во время вычислений (независимо от использования сигналов и слотов) останавливается обработка всех событий. В результате, GUI не обновляется, пользовательский ввод не обрабатывается, сетевая активность прекращается и таймеры не срабатывают – приложение выглядит замороженным и, фактически, та часть приложения, которая не относится к длительным вычислениям и есть заморожена. Насколько длинной должна быть «длинная операция»? Всё, что отвлекает пользователя от взаимодействия с приложением, считается долгим. Одна секунда – это долго; всё, что длиннее двух секунд определённо является «слишком долгим».

Цель этой статьи сохранить функциональность и в тоже время избавить пользователя от замороженного GUI (и сети, и таймеров). Чтобы сделать это, давайте взглянем на возможные классы решений и области проблем.

Мы можем достичь конечной цели наших вычислений двумя путями – либо выполняя вычисления в главном потоке («однопоточный подход»), либо в отдельных потоках («многопоточных подход»). Последнее широко известно и используется в мире Java, но иногда происходит злоупотребление, когда отлично мог бы справиться и один поток. Вопреки популярному мнению, часто потоки могут замедлить ваше приложения вместо того, чтобы ускорить его. Поэтому пока вы не будете уверенны, что ваша программа может выиграть от использования многопоточности (в отношении скорости или простоты), старайтесь избегать создавать новые потоки только потому, что вы это можете. Область проблем может рассматриваться как один из двух случаев. Мы либо можем, либо не можем разделить проблему на меньшие части, вроде шагов, итераций или подзадач (обычно проблема не должна быть монолитной). Если задача может быть разбита на кусочки, каждая из них может быть зависимой либо нет. Если они независимы, мы можем обрабатывать их в любой момент времени и в любом порядке. Иначе, мы должны будем синхронизировать нашу работу. В худшем случае, мы можем делать только один кусочек одновременно и не можем начать следующий, пока не завершится предыдущий. Приняв все эти факторы в рассмотрение, мы можем выбрать различные решения.

[править] Manual event processing

The most basic solution is to explicitly ask Qt to process pending events at some point in the computation. To do this, you have to call QCoreApplication::processEvents() periodically. The following example shows how to do this:

    for (int i = 3; i <= sqrt(x) &amp;&amp; isPrime; i += 2) {
        label->setText(tr("Checking %1...").arg(i));
        if (x % i == 0)
            isPrime = false;
        QCoreApplication::processEvents();
        if (!pushButton->isChecked()) {
            label->setText(tr("Aborted"));
            return;
        }
    }

This approach has significant drawbacks. For example, imagine you wanted to perform two such loops in parallel—calling one of them would effectively halt the other until the first one is finished (so you can't distribute computing power among different tasks). It also makes the application react with delays to events. Furthermore the code is difficult to read and analyze, therefore this solution is only suited for short and simple problems that are to be processed in a single thread, such as splash screens and the monitoring of short operations.

Actually you should call QCoreApplication::sendPostedEvents(). See the documentation for processEvents().

[править] Using a Worker Thread

A different solution is to avoid blocking the main event loop by performing long operations in a separate thread. This is especially useful if the task is performed by a third party library in a blocking fashion. In such a situation it might not be possible to interrupt it to let the GUI process pending events.

One way to perform an operation in a separate thread that gives you most control over the process is to use QThread. You can either subclass it and reimplement its run() method, or call QThread::exec() to start the thread's event loop, or both: subclass and, somewhere in the run() method, call exec(). You can then use signals and slots to communicate with the main thread—just remember that you have to be sure that QueuedConnection will be used or else threads may lose stability and cause your application to crash.

There are many examples of using threads in both the Qt reference documentation and online materials, so we won't make our own implementation, but instead concentrate on other interesting aspects.

[править] Waiting in a Local Event Loop

The next solution I would like to describe deals with waiting until an asynchronous task is completed. Here, I will show you how to block the flow until a network operation is completed without blocking event processing. What we could do is essentially something like this:

    task.start();
    while (!task.isFinished())
        QCoreApplication::processEvents();

This is called busy waiting or spinning—constantly checking a condition until it is met. In most cases this is a bad idea, it tends to eat all your CPU power and has all the disadvantages of manual event processing.

Fortunately, Qt has a class to help us with the task: QEventLoop is the same class that the application and modal dialogs use inside their exec() calls. Each instance of this class is connected to the main event dispatching mechanism and, when its exec() method is invoked, it starts processing events until you tell it to stop using quit().

We can use the mechanism to turn asynchronous operations into synchronous ones using signals and slots—we can start a local event loop and tell it to exit when it sees a particular signal from a particular object:

    QNetworkAccessManager manager;
    QEventLoop q;
    QTimer tT;
 
    tT.setSingleShot(true);
    connect(&tT, SIGNAL(timeout()), &q, SLOT(quit()));
    connect(&manager, SIGNAL(finished(QNetworkReply*)),
            &q, SLOT(quit()));
    QNetworkReply *reply = manager.get(QNetworkRequest(
                   QUrl("http://www.qtcentre.org")));
 
    tT.start(5000); // 5s timeout
    q.exec();
 
    if(tT.isActive()){
        // download complete
        tT.stop();
    } else {
        // timeout
    }

We use one of the new additions to Qt—a network access manager—to fetch a remote URL. As it works in an asynchronous fashion, we create a local event loop to wait for a finished() signal from the downloader. Furthermore, we instantiate a timer that will terminate the event loop after five seconds in case something goes wrong. After connecting appropriate signals, submitting the request and starting the timer we enter the newly created event loop. The call to exec() will return either when the download is complete or five seconds have elapsed (whichever comes first). We find out which is the case by checking if the timer is still running. Then we can process the results or tell the user that the download has failed.

We should note two more things here. Firstly, that a similar approach is implemented in a QxtSignalWaiter class that is part of the libqxt project (http://www.libqxt.org). Another thing is that, for some operations, Qt provides a family of "wait for" methods (for example QIODevice::waitForBytesWritten()) that will do more or less the same as the snippet above but without running an event loop. However, the "wait for" solutions will freeze the GUI because they do not run their own event loops.

[править] Solving a Problem Step by Step

If you can divide your problem into subproblems then there is a nice path you can take to perform the computation without blocking the GUI. You can perform the task in short steps that will not obstruct event processing for longer periods of time. Start processing and, when you notice you have spent some defined time on the task, save its state and return to the event loop. There needs to be a way to ask Qt to continue your task after it is done with events.

Fortunately there is such a way, or even two. One of them is to use a timer with an interval set to zero. This special value will cause Qt to emit the timeout signal on behalf of the timer once its event loop becomes idle. If you connect to this signal with a slot, you will get a mechanism of calling functions when the application is not busy doing other things (similar to how screen savers work). Here is an example of finding prime numbers in the background:

    class FindPrimes : public QObject
    {
        Q_OBJECT
    public:
        FindPrimes(QObject *parent = 0) : QObject(){}
    public slots:
        void start(qlonglong _max);
    private slots:
        void calculate();
    signals:
        void prime(qlonglong);
        void finished();
    private:
        qlonglong cand, max, curr;
        double sqrt;
        void next(){ cand+=2; curr = 3; sqrt = ::sqrt(cand);}
    };
 
    void FindPrimes::start(qlonglong _max)
    {
        emit prime(1); emit prime(2); emit prime(3);
        max = _max; cand = 3; curr = 3;
        next();
        QTimer::singleShot(0, this, SLOT(calculate())); 
    }
 
    void FindPrimes::calculate()
    {
        QTime t;
        t.start();
        while (t.elapsed() < 150) {
            if (cand > max) {
                emit finished();        // end
                return;
            }
            if (curr > sqrt) {
                emit prime(cand);       // prime
                next();
            } else if (cand % curr == 0)
                next();                 // not prime
            else
                curr += 2;              // check next divisor
        }
        QTimer::singleShot(0, this, SLOT(calculate()));
    }

The FindPrimes class makes use of two features—it holds its current calculation state (cand and curr variables) so that it can continue calculations where it left off, and it monitors (by the use of QTime::elapsed()) how long it has been performing the current step of the task. If the time exceeds a predefined amount, it returns to the event loop but, before doing so, it starts a single-shot timer that will call the method again (you might call this approach "deferred recurrency").

I mentioned there were two possibilities of doing a task in steps. The second one is to use QMetaObject::invokeMethod() instead of timers. This method allows you to call any slot from any object. One thing that needs to be said is that, for this to work in our case, we need to make sure the call is made using the Qt::QueuedConnection connection type, so that the slot is called in an asynchronous way (by default, slot calls within a single thread are synchronous). Therefore we might substitute the timer call with the following:

    QMetaObject::invokeMethod(this, "calculate",
                              Qt::QueuedConnection);

The advantage of this over using timers is that you can pass arguments to the slot (for example, passing it the current state of a calculation). Apart from that the two methods are equivalent.

[править] Parallel Programming

Finally, there is the situation where you have to perform a similar operation on a set of data—for instance, creating thumbnails of pictures from a directory. A trivial implementation would look like this:

    QList<QImage> images = loadImages(directory);
    QList<QImage> thumbnails;
    foreach (const QImage &amp;image, images) {
        thumbnails << image.scaled(QSize(300,300), 
            Qt::KeepAspectRatio, Qt::SmoothTransformation);
        QCoreApplication::sendPostedEvents();
    }

A disadvantage of such an approach is that creating a thumbnail of a single image might take quite long, and for that time the GUI would still be frozen. A better approach would be to perform the operation in a separate thread:

    QList<QImage> images = loadImages(directory);
    ThumbThread *thread = new ThumbThread;
    connect(thread, SIGNAL(finished(QList<QImage>)),
            this, SLOT(showThumbnails(QList<QImage>)));
    thread->start(images);

This solution is perfectly fine, but it doesn't take into consideration that computer systems are evolving in a different direction than five or ten years ago—instead of having faster and faster processing units they are being equipped with multiple slower units (multicore or multiprocessor systems) that together offer more computing cycles with lower power consumption and heat emission. Unfortunately, the above algorithm uses only one thread and is thus executed on a single processing unit, which results in slower execution on multicore systems than on single core ones (because a single core in multicore systems is slower than in single core ones).

To overcome this weakness, we must enter the world of parallel programming—we divide the work to be done into as many threads as we have processing units available. Starting with Qt 4.4, there are extensions available to let us do parallel programming: these are provided by QThreadPool and Qt Concurrent.

The first possible course of action is to use so called runnables—simple classes whose instances can be executed by a thread. Qt implements runnables through its QRunnable class. You can implement your own runnable based on the interface offered by QRunnable and execute it using another entity offered by Qt. I mean a thread pool—an object that can spawn a number of threads that execute arbitrary jobs. If the number of jobs exceeds the number of available threads, jobs will be queued and executed when a thread becomes available.

Let's go back to our example and implement a runnable that would create an image thumbnail using a thread pool.

    class ThumbRunnable : public QRunnable {
    public:
        ThumbRunnable(...)  : QRunnable(), ... {}
        void run(){ m_result = m_image.scaled(...); }
        const QImage &amp;result() const{ return m_result; }
    };
 
    QList<ThumbRunnable *> runnables;
    foreach(const QImage &amp;image, images){
        ThumbRunnable *r = new ThumbRunnable(image, ...);
        r->setAutoDelete(false);
        QThreadPool::globalInstance()->start(r);
        runnables << r;
    }

Basically, everything that needs to be done is to implement the run() method from QRunnable. It is done the same way as subclassing QThread, the only difference is that the job is not tied to a thread it creates and thus can be invoked by any existing thread. After creating an instance of ThumbRunnable we make sure it won't be deleted by the thread pool after job execution is completed. We need to to that because we want to fetch the result from the object. Finally, we ask the thread pool to queue the job using the global thread pool available for each application and add the runnable to a list for future reference.

We then have to periodically check each runnable to see if its result is available, which is boring and troublesome, but fortunately there is a better approach when you need to fetch a result. Qt Concurrent introduces a number of paradigms that can be invoked to perform SIMD (Single Instruction Multiple Data) operations. Here we will take a look only at one of them, the simplest one that lets us process each element of a container and have the results ready in another container.

    typedef QFutureWatcher<QImage> ImageWatcher;
    QImage makeThumb(const QString &amp;img)
    {
        return QImage(img).scaled(QSize(300,300), ...);
    }
 
    QStringList images = imageEntries(directory);
    ImageWatcher *watcher = new ImageWatcher(this);
    connect(watcher, SIGNAL(progressValueChanged(int)),
            progressBar, SLOT(setValue(int)));
    QFuture<QImage> result 
        = QtConcurrent::mapped(images, makeThumb);
    watcher->setFuture(result);

Easy, isn't it? Just a few lines of code and the watcher will inform us of the state of the SIMD program held in the QFuture object. It will even let us cancel, pause and resume the program. Here, we used the simplest possible variant of the call—using a standalone function. In a real situation you would use something more sophisticated than a simple function—Qt Concurrent lets you use functions, class methods and function objects. Third party solutions allow you to further extend the possibilities by using bound function arguments.

[править] Conclusion

I have shown you a whole spread of solutions based on type and complexity of problem related to performing time consuming operations in Qt-based programs. Remember that these are only the basics—you can build upon them, for example by creating your own "modal" objects using local event loops, swift data processors using parallel programming, or generic job runners using thread pools. For simple cases, there are ways to manually request the application to process pending events, and for more complex ones dividing the task into smaller subtasks could be the right direction.

You can download complete examples that use techniques described in this article from the Qt Quarterly Web site. If you would like to discuss your solutions, work together with others to solve a problem that's bothering you, or simply spend some time with other Qt (cute?) developers, you can always reach me and others willing to share their knowledge at http://www.qtcentre.org.

Never again let your GUI get frozen!