QtConcurrent--在数以千计的结果发布到UI线程中保持GUI的响应。
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了QtConcurrent--在数以千计的结果发布到UI线程中保持GUI的响应。相关的知识,希望对你有一定的参考价值。
我有一个应用程序,它可能有长期运行的任务,也可能有数千或数百万或结果。
这个特定的应用程序(代码如下)没有任何价值,但它的目的是提供一个一般的使用案例,说明需要在 "成千上万 "的结果中保持一个响应的UI。
要说明的是,我知道应该减少UI的投票次数。我的问题是,在保持响应式UI的过程中,有哪些设计原则可以适用于这种(和其他类似的)情况。
我首先想到的是使用 QTimer 并每隔200毫秒处理一次所有的 "结果",这个例子可以找到 此处 但需要进行美化。
有哪些方法可以使用,哪些是保持响应式UI的首选?
我想解释的一个简单例子如下。我有一个UI。
生成一个整数列表
将其传递到一个映射函数中,以pow(x,2)为值,并且
进展
当运行此应用时,点击 "开始 "按钮就会运行该应用,但由于处理结果的频率由 排队的连接: QFutureWatcher::resultReadyAt因此,UI无法对任何用户的点击做出反应,因此试图 "暂停 "或 "停止"(取消)是徒劳的。
封装器 对于 QtConcurrent::mapped()
传入函数
#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的实施
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;
}
解释为什么UI "没有响应"。
除了打印 "Launching "以外,UI加载wo执行任何操作。当方法 on_thread1Start_clicked()
被调用时,它除了添加下面的连接外,还启动了未来。
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);
}
});
这个连接监听来自未来的结果,并对其进行操作(这个连接函数运行在... UI线). 由于我正在模拟大量的 "ui更新",通过以下方式显示出来 int entries = 50000000;
每次处理一个结果时,都会在 QFutureWatcher<IntStream>::resultReadyAt
被调用。
当它运行+- 2s时,UI不会对链接到的 "暂停 "或 "停止 "点击作出响应。on_thread1PauseResume_clicked()
和 on_thread1Stop_clicked
分别。
你的方法是使用 QtConcurrent::mapped
很有意义,我认为理论上它可以成为解决这样一个问题的好方法。 这里的问题是,添加到事件队列中的事件数量实在是太多了,无法保证UI的响应。
UI不响应的原因是你的GUI线程中只有一个事件队列。 因此,你的按钮 clicked
事件与 resultReadyAt
事件。 但是队列只是一个队列,所以如果你的按钮事件是在30'000'000个resultReadyAt事件之后进入队列的,那么只有轮到它的时候才会被处理。 同样的道理也适用于 resize
和 move
事件。 因此,UI感觉很迟钝,而且反应不灵敏。
一种可能是修改你的映射函数,这样就可以代替单个数据点接收一大批数据。 比如我把50'000'000个数据分成1000个批次的50'000个数据。 你可以看到,在这种情况下,UI在所有执行过程中都是响应的。 我还在每个函数中加入了20ms的延迟,否则执行速度太快,我甚至无法按下停止按钮。
对你的代码也有一些小的意见。
- 原则上,你不需要包装类,因为你可以直接传递成员函数(再看我下面第一个例子)。 如果你有问题,可能与你使用的Qt版本或编译器有关。
- 你实际上是在改变你传递给
doubleValue
. 这实际上使得从函数中返回一个值毫无用处。
#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"
另一个非常好的选择是使用一个单独的工作线程,就像Jeremy Friesner建议的那样。 如果你想的话,我们也可以详细说明一下=)
以上是关于QtConcurrent--在数以千计的结果发布到UI线程中保持GUI的响应。的主要内容,如果未能解决你的问题,请参考以下文章
QtConcurrent blockingMappedReduced 与 MappedReduced
Qt多线程:QtConcurrent + QFuture + QFutureWatcher