Reputation: 2396
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:
generates a list of integers,
passes it into a mapped function to pow(x,2) the value, and
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
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:
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