Qt asynchronous action sequence

2019-06-21 18:40发布

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.

2条回答
一纸荒年 Trace。
2楼-- · 2019-06-21 18:55

How about using QRunnable and QQueue?

QRunnable is a runnable object. You inherit your class from it and reimplement the QRunnable::run() method which will do the 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 (asynchronously) a single task. Asynchronous run can be achieved using the QtConcurrent::run, for example.


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.

查看更多
一纸荒年 Trace。
3楼-- · 2019-06-21 18:58

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.

查看更多
登录 后发表回答