Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Qt asynchronous action sequence

In a C++/Qt program I need to run a few asynchronous tasks with "done" signals (for example network downloads, QProcess, etc.) in sequence, each after the last finishes.

The only ways I can think of are to have a separate state class for each step (extremely verbose, like having a separate class for each line in a synchronous program), or to have one big class with a state enum and fields to hold all possible objects needed for the different steps (inflexible, difficult to maintain). Are there any good solutions for this? It seems like it should be a common problem, but I'm having trouble finding anything.

like image 321
user48145 Avatar asked Jun 19 '16 03:06

user48145


2 Answers

Command pattern

The only ways I can think of are to have a separate state class for each step (extremely verbose)

Actually, there is nothing wrong with this approach. It is called command pattern, and creating a separate class for each action is implied by its design.

You can use QRunnable and QQueue to implement it.

  • QRunnable is a runnable object. You inherit your class from it and reimplement the run() method which will do a single asynchronous job (for example, download a file).
  • QQueue is a simple container which implements the "first in, first out" (FIFO). You may use any other container which fits your needs – QList, QStack, etc.

General implementation

Create a done() signal in you runnable object and emit it at the end of its run() method. To query a new task, simply push your new QRunnable object to the container and connect the done() signal to some slot which will dequeue and run a single task.

If the underlying classes (unlike QProcess, QNetworkManager, etc.) are not asynchronous by design, the asynchronous run can be achieved using the QtConcurrent::run().

See also

You can also use QRunnable with the QThreadPool and manually set the limit of concurrent tasks. Here you can read more about Qt multithreading technologies.

like image 101
kefir500 Avatar answered Sep 23 '22 05:09

kefir500


There are many ways of doing it. One basic pattern is to connect functors to the done() signals:

struct Task : QObject {
   Q_SLOT void start() {}
   Q_SIGNAL void done();
   Q_OBJECT
};

int main(int argc, char ** argv) {
   QCoreApplication app{argc, argv};
   using Q = QObject;
   Task task1, task2, task3;
   Q::connect(&task1, &Task::done, &task2, [&]{ task2.start(); });
   Q::connect(&task2, &Task::done, &task3, [&]{ task3.start(); });
   Q::connect(&task3, &Task::done, &app, [&]{ app.quit(); });
   return app.exec();
}

We can factor out the knowledge about the done signal of a particular class:

template <typename F> void onDone(QProcess * process, QObject * dst, F && f) {
   using signal_type = void(QProcess::*)(int,QProcess::ExitStatus);
   QObject::connect(process, static_cast<signal_type>(&QProcess::finished),
                    dst, std::forward<F>(f));
}

template <typename F> void onDone(QNetworkReply * reply, QObject * dst, F && f) {
   QObject::connect(reply, &QNetworkReply::finished, dst, std::forward<F>(f));
}

int main(int argc, char ** argv) {
   QCoreApplication app{argc, argv};
   QNetworkAccessManager mgr;
   auto download = mgr.get(QNetworkRequest{QUrl{"http://www.google.com"}});
   QProcess process;

   onDone(download, &process, [&]{ process.start(); });
   onDone(&process, &app, [&]{ app.quit(); });

   return app.exec();
}

If there are particular behaviors that are common on a class, or a pair of them, you can factor them out as well. The traits classes help prevent the combinatorial explosion due to multiple possible pairings:

// https://github.com/KubaO/stackoverflown/tree/master/questions/task-sequence-37903585
#include <QtCore>
#include <QtNetwork>
#include <type_traits>

template <typename T> struct SourceAction;
template<> struct SourceAction<QProcess> {
   using signal_type = void(QProcess::*)(int,QProcess::ExitStatus);
   static constexpr signal_type source(QProcess*) {
      return static_cast<signal_type>(&QProcess::finished); }
};
template<> struct SourceAction<QNetworkReply> {
   using signal_type = void(QNetworkReply::*)();
   static constexpr signal_type source(QNetworkReply*) { return &QNetworkReply::finished; }
};

template <typename T> struct TargetAction;
template<> struct TargetAction<QProcess> {
   struct slot_type {
      QProcess * process;
      void operator()() { process->start(); }
      slot_type(QProcess* process) : process(process) {}
   };
   static slot_type destination(QProcess * process) { return slot_type(process); }
};
template<> struct TargetAction<QCoreApplication> {
   using slot_type = void(*)();
   static constexpr slot_type destination(QCoreApplication*) { return &QCoreApplication::quit; }
};

// SFINAE
template <typename Src, typename Dst>
void proceed(Src * src, Dst * dst) {
   QObject::connect(src, SourceAction<Src>::source(src),
                    dst, TargetAction<Dst>::destination(dst));
}
template <typename Src, typename F>
void proceed(Src * src, F && f) {
   QObject::connect(src, SourceAction<Src>::source(src), std::forward<F>(f));
}

QNetworkReply * download(QNetworkAccessManager * mgr, const QUrl & url) {
   return mgr->get(QNetworkRequest{url});
}
QProcess * setup(QProcess * process, const QString & program, const QStringList & args) {
   process->setProgram(program);
   process->setArguments(args);
   return process;
}

int main(int argc, char ** argv) {
   QCoreApplication app{argc, argv};
   if (app.arguments().count() > 1) return 0;

   QNetworkAccessManager mgr;
   QProcess process;

   proceed(download(&mgr, {"http://www.google.com"}), &process);
   proceed(setup(&process, app.applicationFilePath(), {"dummy"}), &app);
   proceed(&process, []{ qDebug() << "quitting"; });
   return app.exec();
}

You can also leverage the state machine system in a similarly declarative fashion.

like image 24
Kuba hasn't forgotten Monica Avatar answered Sep 26 '22 05:09

Kuba hasn't forgotten Monica