QtConcurrent — поддерживает отзывчивость графического интерфейса среди тысяч результатов, опубликованных в потоке пользовательского интерфейса.

У меня есть приложение с потенциально длительными задачами, а также, возможно, с тысячами или миллионами результатов.

Это конкретное приложение (код ниже) не имеет никакой ценности, но оно предназначено для обеспечения общего варианта использования необходимости поддерживать отзывчивый пользовательский интерфейс среди «тысяч» результатов.

Чтобы было ясно, я знаю, что нужно уменьшить количество опросов пользовательского интерфейса. Мой вопрос касается принципов проектирования, которые можно применить к этому (и другим подобным) сценариям, чтобы сохранить адаптивный пользовательский интерфейс.

Моя первая мысль - использовать QTimer и обрабатывать все "результаты" каждый, например. 200 мс, пример можно найти здесь, но нуждается в доработке.

Какие методы доступны и какие предпочтительны для сохранения адаптивного пользовательского интерфейса?


Простой пример, который я пытаюсь объяснить, заключается в следующем. У меня есть пользовательский интерфейс, который:

  1. генерирует список целых чисел,

  2. передает его в сопоставленную функцию для pow(x,2) значения и

  3. измерять прогресс

При запуске этого приложения нажмите кнопку «Пуск», чтобы запустить приложение, но из-за частоты обработки результатов QueuedConnection: QFutureWatcher ::resultReadyAt, пользовательский интерфейс не может реагировать ни на какие клики пользователя, поэтому попытки «приостановить» или «остановить» (отменить) бесполезны.

Оболочка для функции 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::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;
}

Объяснение того, почему пользовательский интерфейс «не отвечает».

Пользовательский интерфейс загружается без выполнения каких-либо действий, кроме печати «Запуск». Когда метод 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);
    }
});

Это подключение прослушивает результат из будущего и действует в соответствии с ним (эта функция подключения выполняется в потоке пользовательского интерфейса). Поскольку я эмулирую огромное количество «обновлений пользовательского интерфейса», показанных int entries = 50000000;, каждый раз, когда обрабатывается результат, вызывается QFutureWatcher<IntStream>::resultReadyAt.

Пока это выполняется в течение +/- 2 с, пользовательский интерфейс не реагирует на клики «пауза» или «стоп», связанные с on_thread1PauseResume_clicked() и on_thread1Stop_clicked соответственно.


person CybeX    schedule 02.06.2020    source источник
comment
Возможно, вы могли бы создать специальный поток обработки результатов и направить в него свои миллионы результатов. Он может выполнять любую первоначальную обработку результатов и собирать их вместе в исполнительные сводки, которые будут передаваться потоку графического интерфейса через более длительные интервалы времени.   -  person Jeremy Friesner    schedule 02.06.2020


Ответы (1)


Ваш подход к использованию QtConcurrent::mapped имеет смысл, и я думаю, что теоретически это может быть хорошим способом решения такой проблемы. Проблема здесь в том, что количество событий, добавляемых в очередь событий, слишком велико, чтобы поддерживать отзывчивость пользовательского интерфейса.

Причина, по которой пользовательский интерфейс не отвечает, заключается в том, что у вас есть только одна очередь событий в потоке графического интерфейса. Как следствие, события вашей кнопки clicked ставятся в очередь вместе с событиями resultReadyAt. Но очередь - это просто очередь, поэтому, если ваше событие кнопки входит в очередь после, скажем, 30 000 000 событий resultReadyAt, оно будет обработано только тогда, когда придет его очередь. То же самое относится к событиям resize и move. Как следствие, пользовательский интерфейс кажется вялым и не отзывчивым.

Одной из возможностей было бы изменить вашу функцию сопоставления, чтобы вместо одной точки данных получался фрагмент данных. Например, я разбиваю 50 000 000 данных на 1000 пакетов по 50 000 данных. Вы можете видеть, что в этом случае пользовательский интерфейс реагирует на протяжении всего выполнения. Я также добавил задержку в 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"

Другой действительно хорошей альтернативой может быть использование отдельного рабочего потока, предложенного Джереми Фриснером. Если хотите, мы можем рассказать и об этом =)

person William Spinelli    schedule 03.06.2020
comment
спасибо за ответ - я подумал, что либо создание подкласса потока, либо добавление задержки (чего я не хочу делать) поможет, но добавление задержки X мс добавит задержку nX мс. Когда мы говорим о 5 миллионах «задач», если добавить 20 мс ожидания на выполнение (при условии, что 12 потоков — которые, как я обнаружил, я использовал), я буду ждать 2,3 минуты, чтобы завершить выполнение, в то время как без задержки оно завершается в возрасте до 10 лет. (5mil * 20ms) / 1000(ms) / 60(sec) / 60(min) /12(threads) - person CybeX; 03.06.2020
comment
Я добавил задержку только для того, чтобы показать, что решение работает без блокировки пользовательского интерфейса. Нет необходимости добавлять задержку в реальной реализации. Но если я не добавлю задержку в этой реализации, выполнение будет настолько быстрым, что я даже не смогу нажать кнопку остановки или паузы. Весь смысл в том, чтобы не публиковать слишком много событий в потоке пользовательского интерфейса. - person William Spinelli; 03.06.2020