1

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

这个特定的应用程序(下面的代码)没有任何价值,但它旨在提供一个需要在“数千个”结果中维护响应式 UI 的一般用例。

需要明确的是,我知道应该减少轮询 UI 的次数。我的问题是关于可以应用于此(和其他类似)场景以保持响应式 UI 的设计原则。

我的第一个想法是使用QTimer并每隔 200 毫秒处理一次所有“结果”,可以在此处找到一个示例,但需要添加。

哪些方法可用,哪些方法更适合保持响应式 UI?


我试图解释的一个简单例子如下。我有一个用户界面:

  1. 生成一个整数列表,

  2. 将其传递给映射函数以 pow(x,2) 值,并且

  3. 衡量进度

运行此应用程序时,单击“开始”按钮将运行该应用程序,但由于QueuedConnection : QFutureWatcher::resultReadyAt处理结果的频率,UI 无法响应任何用户点击,因此尝试“暂停”或“停止”(取消)是徒劳的。

传入 lambda的函数的包装器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 用户界面

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(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 没有“响应”。

UI 加载时不执行任何操作,而不是打印“Launching”。调用该方法时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都会调用 。

虽然它运行了 +/- 2 秒,但 UI 不会响应分别链接到on_thread1PauseResume_clicked()和的“暂停”或“停止”点击on_thread1Stop_clicked

4

1 回答 1

1

您的使用方法QtConcurrent::mapped非常有意义,我认为理论上它可能是解决此类问题的好方法。这里的问题是添加到事件队列的事件数量太多,无法保持 UI 响应。

UI 没有响应的原因是您在 GUI 线程中只有一个事件队列。因此,您的按钮clicked事件与事件一起排队resultReadyAt。但是队列就是这样,一个队列,所以如果你的按钮事件在 resultReadyAt 事件的 30'000'000 之后进入队列,它只会在轮到它的时候被处理。事件也是resize如此。move因此,用户界面感觉迟缓且没有响应。

一种可能性是修改您的映射函数,以便代替单个数据点接收一大块数据。例如,我将 50'000'000 数据拆分为 1000 批 50'000 数据。您可以看到,在这种情况下,UI 在所有执行过程中都是响应式的。我还在每个函数中添加了 20 毫秒的延迟,否则执行速度非常快,我什至无法按下停止/暂停按钮。

您的代码还有一些小注释:

  • 原则上您不需要包装类,因为您可以直接传递成员函数(再次参见下面的第一个示例)。如果您有问题,可能与您使用的 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 建议的单独的工作线程。如果您愿意,我们也可以详细说明=)

于 2020-06-03T14:08:58.000 回答