CybeX
CybeX

Reputation: 2396

QtConcurrent - keeping GUI responsive amid thousands of results posted to UI thread

I have an application that has potentially long-running tasks and also possibly thousands or millions or results.

This specific application (code below) isn't of any worth, but it is aimed to provide a general use case of the need to maintain a responsive UI amid 'thousands' of results.

To be clear, I am aware that one should reduce the number of times the UI is polled. My question is regarding design principles that can be applied to this (and other similar) scenarios in keeping a responsive UI.

My first thought is to use a QTimer and process all 'results' every e.g. 200ms, an example which can be found here but needs adation.

What methods are available and which are preferred to keep a responsive UI?


A simple example of I am trying to explain is as follows. I have a UI that:

  1. generates a list of integers,

  2. passes it into a mapped function to pow(x,2) the value, and

  3. measure the progress

When running this app, click the 'start' button will run the application, but due to the frequency of results being processed by the QueuedConnection: QFutureWatcher::resultReadyAt, the UI cannot respond to any user clicks, thus attempting to 'pause' or 'stop' (cancel) is futile.

Wrapper for QtConcurrent::mapped() function passing in lambda (for a member function)

#include <functional>

template <typename ResultType>
class MappedFutureWrapper
{
public:
    using result_type = ResultType;

    MappedFutureWrapper<ResultType>(){}
    MappedFutureWrapper<ResultType>(std::function<ResultType (ResultType)> function): function(function){ }
    MappedFutureWrapper& operator =(const MappedFutureWrapper &wrapper) {
        function = wrapper.function;
        return *this;
    }
    ResultType operator()(ResultType i) {
        return function(i);
    }

private:
    std::function<ResultType(ResultType)> function;
};

MainWindow.h UI

class MainWindow : public QMainWindow {
     Q_OBJECT

  public:
     struct IntStream {
         int value;
     };

     MappedFutureWrapper<IntStream> wrapper;
     QVector<IntStream> intList;

     int count = 0;
     int entries = 50000000;

     MainWindow(QWidget* parent = nullptr);
     static IntStream doubleValue(IntStream &i);
     ~MainWindow();

    private:
       Ui::MainWindow* ui;
       QFutureWatcher<IntStream> futureWatcher;
       QFuture<IntStream> future;

       //...
}

MainWindow implementation

MainWindow::MainWindow(QWidget* parent)
     : QMainWindow(parent)
     , ui(new Ui::MainWindow)
{
     ui->setupUi(this);
    qDebug() << "Launching";

     intList = QVector<IntStream>();
     for (int i = 0; i < entries; i++) {
         int localQrand = qrand();
         IntStream s;
         s.value = localQrand;
         intList.append(s);
     }

     ui->progressBar->setValue(0);

}

MainWindow::IntStream MainWindow::doubleValue(MainWindow::IntStream &i)
{
    i.value *= i.value;
    return i;
}

void MainWindow::on_thread1Start_clicked()
{
    qDebug() << "Starting";

    // Create wrapper with member function
    wrapper = MappedFutureWrapper<IntStream>([this](IntStream i){
        return this->doubleValue(i);
    });

    // Process 'result', need to acquire manually
    connect(&futureWatcher, &QFutureWatcher<IntStream>::resultReadyAt, [this](int index){
        auto p = ((++count * 1.0) / entries * 1.0) * 100;
        int progress = static_cast<int>(p);
        if(this->ui->progressBar->value() != progress) {
            qDebug() << "Progress = " << progress;
            this->ui->progressBar->setValue(progress);
        }
    });

    // On future finished
    connect(&futureWatcher, &QFutureWatcher<IntStream>::finished, this, [](){
        qDebug() << "done";
    });

    // Start mapped function
    future = QtConcurrent::mapped(intList, wrapper);
    futureWatcher.setFuture(future);
}

void MainWindow::on_thread1PauseResume_clicked()
{
    future.togglePaused();
    if(future.isPaused()) {
        qDebug() << "Paused";
    } else  {
        qDebug() << "Running";
    }
}

void MainWindow::on_thread1Stop_clicked()
{
    future.cancel();
    qDebug() << "Canceled";

    if(future.isFinished()){
        qDebug() << "Finished";
    } else {
        qDebug() << "Not finished";
    }

}

MainWindow::~MainWindow()
{
     delete ui;
}

Explanation of why the UI is 'not responding'.

The UI loads w/o performing any action other than printing "Launching". When the method on_thread1Start_clicked() is invoked, it started the future, in addition to adding the following connection:

connect(&futureWatcher, &QFutureWatcher<IntStream>::resultReadyAt, [this](int index){
    auto p = ((++count * 1.0) / entries * 1.0) * 100;
    int progress = static_cast<int>(p);
    if(this->ui->progressBar->value() != progress) {
        qDebug() << "Progress = " << progress;
        this->ui->progressBar->setValue(progress);
    }
});

This connection listens for a result from the future, and acts upon it (this connect function runs on the UI thread). Since I am emulating a massive amount of 'ui updates', shown by int entries = 50000000;, each time a result is processed, the QFutureWatcher<IntStream>::resultReadyAt is invoked.

While this is running for +/- 2s, the UI does not respond to the 'pause' or 'stop' clicks linked to on_thread1PauseResume_clicked() and on_thread1Stop_clicked respectively.

Upvotes: 1

Views: 611

Answers (1)

William Spinelli
William Spinelli

Reputation: 439

Your approach of using QtConcurrent::mapped makes perfect sense, and I think that in theory it could be a good way of solving such a problem. The problem here is that the number of events that are added to the event queue are just too much to keep the UI responsive.

The reason why the UI is not responding is that you have only one event queue in the GUI thread. As a consequence your button clicked events are queued together with the resultReadyAt events. But the queue is just that, a queue, so if your button event enter the queue after say 30'000'000 of resultReadyAt event, it will be processed only when it comes its turn. The same holds for resize and move events. As a consequence the UI feels sluggish and not responsive.

One possibility would be to modify your mapping function so that instead of a single data point receives a chunk of the data. For example I'm splitting the 50'000'000 data in 1000 batch of 50'000 data. You can see that in this case the UI is responsive during all the execution. I have also added a 20ms delay in each function otherwise the execution is so fast that I cannot even press the stop/pause button.

There are also a couple of minor comments to your code:

  • In principle you don't need a wrapper class since you can pass the member function directly (again see my first example below). If you have problem maybe it's related to the Qt version or compiler that you are using.
  • You are actually changing the value you pass to doubleValue. That actually makes useless returning a value from the function.
#include <QApplication>
#include <QMainWindow>
#include <QProgressBar>
#include <QPushButton>
#include <QRandomGenerator>
#include <QtConcurrent>
#include <QVBoxLayout>


class Widget : public QWidget {
    Q_OBJECT

public:
    struct IntStream {
        int value;
    };

    Widget(QWidget* parent = nullptr);
    static QVector<IntStream> doubleValue(const QVector<IntStream>& v);

public slots:
    void startThread();
    void pauseResumeThread();
    void stopThread();

private:
    static constexpr int                BATCH_SIZE {50000};
    static constexpr int                TOTAL_BATCHES {1000};
    QFutureWatcher<QVector<IntStream>>  m_futureWatcher;
    QFuture<QVector<IntStream>>         m_future;
    QProgressBar                        m_progressBar;
    QVector<QVector<IntStream>>         m_intList;
    int                                 m_count {0};
};


Widget::Widget(QWidget* parent) : QWidget(parent)
{
    auto layout {new QVBoxLayout {}};

    auto pushButton_startThread {new QPushButton {"Start Thread"}};
    layout->addWidget(pushButton_startThread);
    connect(pushButton_startThread, &QPushButton::clicked,
            this, &Widget::startThread);

    auto pushButton_pauseResumeThread {new QPushButton {"Pause/Resume Thread"}};
    layout->addWidget(pushButton_pauseResumeThread);
    connect(pushButton_pauseResumeThread, &QPushButton::clicked,
            this, &Widget::pauseResumeThread);

    auto pushButton_stopThread {new QPushButton {"Stop Thread"}};
    layout->addWidget(pushButton_stopThread);
    connect(pushButton_stopThread, &QPushButton::clicked,
            this, &Widget::stopThread);

    layout->addWidget(&m_progressBar);

    setLayout(layout);

    qDebug() << "Launching";

    for (auto i {0}; i < TOTAL_BATCHES; i++) {
        QVector<IntStream> v;
        for (auto j {0}; j < BATCH_SIZE; ++j)
            v.append(IntStream {static_cast<int>(QRandomGenerator::global()->generate())});
        m_intList.append(v);
    }
}

QVector<Widget::IntStream> Widget::doubleValue(const QVector<IntStream>& v)
{
    QThread::msleep(20);
    QVector<IntStream> out;
    for (const auto& x: v) {
        out.append(IntStream {x.value * x.value});
    }
    return out;
}

void Widget::startThread()
{
    if (m_future.isRunning())
        return;
    qDebug() << "Starting";

    m_count = 0;

    connect(&m_futureWatcher, &QFutureWatcher<IntStream>::resultReadyAt, [=](int){
        auto progress {static_cast<int>(++m_count * 100.0 / TOTAL_BATCHES)};
        if (m_progressBar.value() != progress && progress <= m_progressBar.maximum()) {
            m_progressBar.setValue(progress);
        }
    });

    connect(&m_futureWatcher, &QFutureWatcher<IntStream>::finished,
            [](){
                qDebug() << "Done";
            });

    m_future = QtConcurrent::mapped(m_intList, &Widget::doubleValue);
    m_futureWatcher.setFuture(m_future);
}

void Widget::pauseResumeThread()
{
    m_future.togglePaused();

    if (m_future.isPaused())
        qDebug() << "Paused";
    else
        qDebug() << "Running";
}

void Widget::stopThread()
{
    m_future.cancel();
    qDebug() << "Canceled";

    if (m_future.isFinished())
        qDebug() << "Finished";
    else
        qDebug() << "Not finished";
}


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

#include "main.moc"

Another really good alternative could be using a separate working thread as suggested by Jeremy Friesner. If you want we can elaborate on that too =)

Upvotes: 1

Related Questions