QtConcurrent--在数以千计的结果发布到UI线程中保持GUI的响应。

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了QtConcurrent--在数以千计的结果发布到UI线程中保持GUI的响应。相关的知识,希望对你有一定的参考价值。

我有一个应用程序,它可能有长期运行的任务,也可能有数千或数百万或结果。

这个特定的应用程序(代码如下)没有任何价值,但它的目的是提供一个一般的使用案例,说明需要在 "成千上万 "的结果中保持一个响应的UI。

要说明的是,我知道应该减少UI的投票次数。我的问题是,在保持响应式UI的过程中,有哪些设计原则可以适用于这种(和其他类似的)情况。

我首先想到的是使用 QTimer 并每隔200毫秒处理一次所有的 "结果",这个例子可以找到 此处 但需要进行美化。

有哪些方法可以使用,哪些是保持响应式UI的首选?


我想解释的一个简单例子如下。我有一个UI。

  1. 生成一个整数列表

  2. 将其传递到一个映射函数中,以pow(x,2)为值,并且

  3. 进展

当运行此应用时,点击 "开始 "按钮就会运行该应用,但由于处理结果的频率由 排队的连接: 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事件之后进入队列的,那么只有轮到它的时候才会被处理。 同样的道理也适用于 resizemove 事件。 因此,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

Qt 如何在 QtConcurrent 中将指针绑定到非静态成员函数?

如何提高性能结果的精度

如何设置 QTConcurrent 操作的最大线程数?

PySide/PyQt 中的 QtConcurrent