Reputation: 103
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.
Upvotes: 6
Views: 2381
Reputation: 4404
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.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()
.
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.
Upvotes: 4
Reputation: 98425
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.
Upvotes: 2