QT Многопоточность и обновление GUI - PullRequest
1 голос
/ 03 марта 2020

В настоящее время я обновляю существующую кодовую базу, предназначенную для использования с GTK GUI, до QT, чтобы она могла реализовывать многопоточность, поскольку функции выполняются часами.

Эта кодовая база часто используется вызывает функцию display(std::string) с целью обновления виджета для отображения текста. Я переопределил эту функцию для новой версии QT:

In Display. cpp:

void display(std::string output)
{
    //
    MainWindow * gui = MainWindow::getMainWinPtr(); //Gets instance of GUI
    gui->DisplayInGUI(output); //Sends string to new QT display function
}

In MainWindow. cpp:

void MainWindow::DisplayInGUI(std::string output)
{
    //converts output to qstring and displays in text edit widget
}

void MainWindow::mainFunction(){
    //calls function in existing codebase, which itself is frequently calling display()
}

void MainWindow::on_mainFunctionButton_released()
{
    QFuture<void> future = QtConcurrent::run(this,&MainWindow::mainFunction);
}

If I запустите основную функцию в новом потоке, display(std::string) не будет обновлять GUI, пока поток не завершится. Я понимаю почему; GUI может быть обновлено только в главном потоке. Все остальное работает как задумано. То, что я хочу реализовать, но я не уверен, каким образом, display(std:string) посылает сигнал обратно в основной поток для вызова MainWindow::DisplayInGUI(output_text) со строкой, которая была передана функции display (). Я считаю, что это правильный способ сделать это, но поправьте меня, если я ошибаюсь. Я хочу избежать изменения существующей кодовой базы любой ценой.

РЕДАКТИРОВАТЬ: я должен добавить, что по некоторым глупым причинам полностью вне моего контроля, я вынужден использовать C ++ 98 (да, я знаю)

Ответы [ 3 ]

1 голос
/ 03 марта 2020

Прежде всего: нет способа сделать потокобезопасным метод getMainWinPtr, поэтому этот псевдо-синглтон-хак, вероятно, должен go удалиться. Вы можете передать некоторый глобальный контекст приложения всем объектам, которые делают глобальные приложения, такие как предоставление обратной связи с пользователем. Скажем, есть MyApplication : QObject (не производные от QApplication, это не нужно). Это можно обойти при создании новых объектов, а затем вы сможете контролировать относительное время жизни вовлеченных объектов непосредственно в функции main():

void main(int argc, char **argv) {
  QApplication app(argc, argv);
  MainWindow win;
  MyApplication foo;
  win.setApplication(&foo);
  // it is now guaranteed by the semantics of the language that 
  // the main window outlives `MyApplication`, and thus `MyApplication` is free to assume
  // that the window exists and it's OK to call its methods
  ...
  return app.exec();
}

Конечно, MyApplication должен позаботиться о том, чтобы рабочие потоки останавливаются до того, как его деструктор возвращается.

Для передачи асинхронных изменений в QObject, проживающих в (не перегруженных) QThread s (включая основной поток), используйте встроенный внутренний поток коммуникация, присущая дизайну Qt: события и слот вызывают пересекающие границы потоков.

Итак, учитывая метод DisplayInGUI, вам необходим потокобезопасный способ его вызова:

std::string newOutput = ...;
QMetaObject::invokeMethod(mainWindow, [mainWindow, newOutput]{
  mainWindow->displayInGUI(newOutput);
});

Это учитывает аспект безопасности потока. Теперь у нас есть еще одна проблема: главное окно может быть забито этими обновлениями гораздо быстрее, чем частота обновления экрана sh, поэтому нет смысла в потоке уведомлять главное окно чаще, чем с какой-то разумной скоростью, он просто тратит ресурсы .

Лучше всего это сделать, сделав метод DisplayInGUI поточно-ориентированным и используя API синхронизации в Qt:

class MainWindow : public QWidget {
  Q_OBJECT
  ...
  static constexpr m_updatePeriod = 1000/25; // in ms
  QMutex m_displayMutex;
  QBasicTimer m_displayRefreshTimer;
  std::string m_newDisplayText;
  bool m_pendingRefresh;
  ...

  void timerEvent(QTimerEvent *event) override {
    if (event->timerId() == m_displayRefreshTimer.timerId()) {
      QMutexLocker lock(&m_displayMutex);
      std::string text = std::move(m_newDisplayText);
      m_pendingRefresh = false;
      lock.release();
      widget->setText(QString::fromStdString(text));
    }
    QWidget::timerEvent(event);
  }

  void DisplayInGUI(const std::string &str) {
    // Note pass-by-reference, not pass-by-value. Pass by value gives us no benefit here.
    QMutexLocker lock(&m_displayMutex);
    m_newDisplayText = str;
    if (m_pendingRefresh) return;
    m_pendingRefresh = true;
    lock.release();

    QMetaObject::invokeMethod(this, &MainWindow::DisplayInGui_impl);
  }
private:
  Q_SLOT void DisplayInGui_impl() {
    if (!m_displayRefreshTimer.isActive()) 
      m_displayRefreshTimer.start(this, m_updatePeriod);
  }
};

В более сложной ситуации вы, вероятно, захотите вычеркните настройку свойства cross-thread для некоторого «вспомогательного» класса, который будет выполнять такие операции без шаблона.

1 голос
/ 03 марта 2020

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

#include <QApplication>
#include <QtGlobal>
#include <utility>

template<typename F>
void runInMainThread(F&& fun)
{
    QObject tmp;
    QObject::connect(&tmp, &QObject::destroyed, qApp, std::forward<F>(fun),
                     Qt::QueuedConnection);
}

Теперь вы можете запустить код (используя лямбду в этом примере, но любой другой вызываемый объект будет работать) в главном потоке, например так:

runInMainThread([] { /* code */ });

В вашем случае:

void display(std::string output)
{
    runInMainThread([output = std::move(output)] {
        MainWindow* gui = MainWindow::getMainWinPtr();
        gui->DisplayInGUI(output);
    });
}

Или вы можете оставить display() как есть и вместо этого обернуть вызовы к нему:

runInMainThread([str] { display(std::move(str)); );

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

Это не высокопроизводительный механизм связи между потоками. Каждый вызов приведет к созданию временного объекта QObject и временного соединения сигнал / слот. Для периодических обновлений пользовательского интерфейса c этого достаточно, и он позволяет вам запускать любой код в основном потоке без необходимости вручную устанавливать соединения сигнал / слот для различных операций обновления пользовательского интерфейса. Но для тысяч вызовов пользовательского интерфейса в секунду это, вероятно, не очень эффективно.

0 голосов
/ 03 марта 2020

Вы можете воспользоваться тем, что QTimer::singleShot имеет перегрузку, которая при вызове с нулевым интервалом времени позволяет эффективно планировать выполнение задачи в указанном потоке в течение этого потока. следующий свободный слот ...

void QTimer::singleShot(int msec, const QObject *context, Functor functor);

Так что ваш MainWindow::mainFunction может быть чем-то вроде ...

void MainWindow::mainFunction ()
{
    ...
    std::string output = get_ouput_from_somewhere();
    QTimer::singleShot(0, QApplication::instance(),
                       [output]()
                       {
                           display(output);
                       });
    ...
}
...