Ссылки на статьи
Предисловие
Во всех предыдущих статьях мы рассматривали лишь самый простой пример — последовательный вывод сообщений на экран в отдельном потоке.
Пришло время, наконец, сделать что-то более реальное и существенное, пусть и не очень сложное. И этим будет менеджер http get запросов.
Зачем оно нужно
Предположим, что мы хотим сначала выполнить некоторую задачу (возьмём вывод "Hello, world"), а после того, как эту задачу выполним, загрузить из сети некоторые данные (возмём страницу документации Qt), и вывести размер загруженных данных в байтах (не будем выводить сами данные, чтобы не загромождать вывод).
Попытка №1
Первой попыткой может стать следующий код
Попытка сделать запрос
auto printer = std::make_shared<EventBasedAsyncQDebugPrinter>();
auto httpManager = std::make_shared<QNetworkAccessManager>();
auto printerThread = new QThread{};
printer->moveToThread(printerThread);
printerThread->start();
auto httpThread = new QThread{};
httpManager->moveToThread(httpThread);
httpThread->start();
printer->print("Hello, world!").
then(QtFuture::Launch::Async, [printer, httpManager]() {
auto reply = httpManager->get(QNetworkRequest{ QUrl{ "https://doc.qt.io/qt-6/qnetworkaccessmanager.html" } });
return QtFuture::connect(reply, &QNetworkReply::finished).
then([replyPointer = std::shared_ptr<QNetworkReply>{ reply, QScopedPointerDeleteLater{} }, printer] {
printer->print(QString::number(replyPointer->readAll().size()));
});
}).unwrap().then([printer](){
printer->print("Application work finished [success]");
}).onCanceled([printer] {
printer->print("Application work finished [cancel]");
}).onFailed([printer]{
printer->print("Application work finished [fail]");
});
Сначала создаём shared_pointer на менеджер запросов и active object принтера, затем выводим "Hello, world!", после чего пытаемся сделать get-запрос из менеджера, и в конце приложения выводим результат его работы (success, cancel или fail).
Не забываем и про то, что когда мы создаём QObject в многопоточной среде, мы не должны ни в коем случае дёргать delete. Вместо этого нужно использовать метод deleteLater. Поэтому в строке 8 при создании shared_ptr вторым параметром передаётся стандартный Qt-шный deleter, который вызывает deleteLater. Объекты printer и httpManager живут на протяжении всего времени работы приложения, поэтому выдавать deleter им нет смысла.
Результат запуска нас огорчит.
![](https://habrastorage.org/getpro/habr/upload_files/9ac/1f1/998/9ac1f19988dd4661ae61cc07772726b9.png)
Мы не можем сделать вызов метода get из потока, который не владеет менеджером. Это связано с тем, что метод get создаст объект QNetworkRequest, являющийся наследником httpManager. Qt не рекомендует добавление QObject в дерево объектов, не принадлежащих потоку. Это может работать, но будет undefined behaviour.
Попытка №2
Тогда мы попробуем создать и httpManager, и запрос в continuation. Ведь тогда они будут в одном потоке, и жизнь будет легка и хороша.
Ещё одна попытка сделать запрос
auto printer = std::make_shared<EventBasedAsyncQDebugPrinter>();
auto printerThread = new QThread{};
printer->moveToThread(printerThread);
printerThread->start();
printer->print("Hello, world!").
then(QtFuture::Launch::Async, [printer]() {
auto httpManager = std::make_shared<QNetworkAccessManager>();
auto reply = httpManager->get(QNetworkRequest{ QUrl{ "https://doc.qt.io/qt-6/qnetworkaccessmanager.html" } });
return QtFuture::connect(reply, &QNetworkReply::finished).
then([httpManager, replyPointer = std::shared_ptr<QNetworkReply>{ reply, QScopedPointerDeleteLater{} }, printer] {
printer->print(QString::number(replyPointer->readAll().size()));
});
}).unwrap().then([printer](){
printer->print("Application work finished [success]");
}).onCanceled([printer] {
printer->print("Application work finished [cancel]");
}).onFailed([printer]{
printer->print("Application work finished [fail]");
});
Не забываем добавить httpManager в список захвата лямбды, чтобы он не был удалён раньше времени.
Но и теперь у нас ничего не выйдет.
![](https://habrastorage.org/getpro/habr/upload_files/711/067/3fe/7110673fe60b3192deafcb72775e07ae.png)
На этот раз всё сломалось из-за флага QtFuture::Launch::Async во второй строке. Он заставил continuation (лямбду) выполняться на одном из потоков QThreadPool::globalInstance(). Этот поток обёрнут в QThreadPoolThread. А этот класс не предполагает запуск цикла событий. А значит, любой QObject, принадлежащий этому потоку, будет мёртв и не сможет обмениваться событиями, сигналами и слотами.
Попытка №3
Хорошо. Тогда мы отключим исполнение на QThreadPool и заставим continuation выполняться силами потока-владельца active object.
Создаём менеджер прямо в continuation.
auto printer = std::make_shared<EventBasedAsyncQDebugPrinter>();
auto printerThread = new QThread{};
printer->moveToThread(printerThread);
printerThread->start();
printer->print("Hello, world!").
then([printer]() {
auto httpManager = std::make_shared<QNetworkAccessManager>();
auto reply = httpManager->get(QNetworkRequest{ QUrl{ "https://doc.qt.io/qt-6/qnetworkaccessmanager.html" } });
return QtFuture::connect(reply, &QNetworkReply::finished).
then([httpManager, replyPointer = std::shared_ptr<QNetworkReply>{ reply, QScopedPointerDeleteLater{} }, printer] {
printer->print(QString::number(replyPointer->readAll().size()));
});
}).unwrap().then([printer](){
printer->print("Application work finished [success]");
}).onCanceled([printer] {
printer->print("Application work finished [cancel]");
}).onFailed([printer]{
printer->print("Application work finished [fail]");
});
![](https://habrastorage.org/getpro/habr/upload_files/f89/4d1/7ff/f894d17fff34fad7ed3eda8a446ac873.png)
И да. Это работает. Но мы имеем крайне мерзкую нежизнеспособную систему, в которой на каждый запрос придётся делать свой QNetworkAccessManager.
Небольшое отступление
Второй пример точно также не работал из-за QtFuture::Launch::Async. Если убрать его, то картина будет следующей
Запуск без QtFuture::Launch::Async
auto printer = std::make_shared<EventBasedAsyncQDebugPrinter>();
auto httpManager = std::make_shared<QNetworkAccessManager>();
auto printerThread = new QThread{};
printer->moveToThread(printerThread);
printerThread->start();
auto httpThread = new QThread{};
httpManager->moveToThread(httpThread);
httpThread->start();
printer->print("Hello, world!").
then([printer, httpManager]() {
auto reply = httpManager->get(QNetworkRequest{ QUrl{ "https://doc.qt.io/qt-6/qnetworkaccessmanager.html" } });
return QtFuture::connect(reply, &QNetworkReply::finished).
then([replyPointer = std::shared_ptr<QNetworkReply>{ reply, QScopedPointerDeleteLater{} }, printer] {
printer->print(QString::number(replyPointer->readAll().size()));
});
}).unwrap().then([printer](){
printer->print("Application work finished [success]");
}).onCanceled([printer] {
printer->print("Application work finished [cancel]");
}).onFailed([printer]{
printer->print("Application work finished [fail]");
});
![](https://habrastorage.org/getpro/habr/upload_files/c8c/8da/ad6/c8c8daad60608971ee33346ccdb10dbe.png)
Проблема в том, что мы всё равно создаём QObject в методе get из потока, который не владеет httpManager. Конкретно здесь, на моей машине, в моей версии Qt, в этом коде это не выстрелило. Но это undefined behaviour, и делать так нельзя.
Итого, имеем, что без танцев с бубной мы можем либо не параллелить вовсе, либо создавать свой менеджер на каждый запрос, что грустно. Именно поэтому мы создадим Active Object, занимающийся обработкой http-запросов.
Реализация HttpManager
Он будет основан на идее EventBasedAsyncQDebugPrinter из предыдущей статьи.
HttpManager
//HttpManager.h
class HttpManager : public QObject {
private:
class HttpGetRequestEvent : public QEvent {
private:
QPromise<std::shared_ptr<QNetworkReply>> m_promise;
const QNetworkRequest m_request;
public:
inline static constexpr QEvent::Type Type = static_cast<QEvent::Type>(QEvent::Type::User + 1);
HttpGetRequestEvent(const QNetworkRequest &request);
public:
QPromise<std::shared_ptr<QNetworkReply>>& promise();
const QNetworkRequest& request() const;
};
private:
QPointer<QNetworkAccessManager> m_manager;
public:
explicit HttpManager(QObject *parent = nullptr);
QFuture<std::shared_ptr<QNetworkReply>> get(const QNetworkRequest& request);
protected:
virtual void customEvent(QEvent *event) override;
};
//HttpManager.cpp
HttpManager::HttpGetRequestEvent::HttpGetRequestEvent(const QNetworkRequest &request)
:QEvent{ Type }, m_request{ std::move(request) } {}
QPromise<std::shared_ptr<QNetworkReply>> &HttpManager::HttpGetRequestEvent::promise() {
return m_promise;
}
const QNetworkRequest &HttpManager::HttpGetRequestEvent::request() const {
return m_request;
}
HttpManager::HttpManager(QObject *parent)
:QObject{ parent }, m_manager{ new QNetworkAccessManager{ this } } {}
QFuture<std::shared_ptr<QNetworkReply>> HttpManager::get(const QNetworkRequest& request) {
auto task = new HttpGetRequestEvent{ std::move(request) };
auto future = task->promise().future();
qApp->postEvent(this, task);
return future;
}
void HttpManager::customEvent(QEvent *event) {
if(auto request = dynamic_cast<HttpGetRequestEvent*>(event); request) {
auto reply = std::shared_ptr<QNetworkReply>{ m_manager->get(request->request()), QScopedPointerDeleteLater{} };
QtFuture::connect(reply.get(), &QNetworkReply::finished).
then([reply, reply_promise = std::move(request->promise())]() mutable ->void {
reply_promise.addResult(reply);
reply_promise.finish();
});
}
}
HttpGetRequestEvent является просто dto для передачи запроса между потоками.
В метод get передаётся объект QNetworkRequest, содержащий все данные для совершения get-запроса, создаётся HttpGetRequestEvent, берётся future от него, событие-задача отправляется самому себе через postEvent(), после чего вызывающей стороне возвращается future на результат запроса.
При этом, когда результат оборачивается в shared_ptr, желательно добавить QScopedPointerDeleteLater{}, как того просит документация Qt.
В методе custonEvent происходит отлов события и выполнение запроса. Когда запрос будет выполнен, его результат будет записан в promise.
Использование класса крайне сильно похоже на использование QNetworkAccessManager.
Использование HttpManager
auto printer = std::make_shared<EventBasedAsyncQDebugPrinter>();
auto httpManager = std::make_shared<HttpManager>();
auto printerThread = new QThread{};
printer->moveToThread(printerThread);
printerThread->start();
auto httpThread = new QThread{};
httpManager->moveToThread(httpThread);
httpThread->start();
printer->print("Hello, world!").
then(QtFuture::Launch::Async, [printer, httpManager]() {
return httpManager->get(QNetworkRequest{ QUrl{ "https://doc.qt.io/qt-6/qnetworkaccessmanager.html" } }).
then([printer](std::shared_ptr<QNetworkReply> reply) {
printer->print(QString::number(reply->readAll().size()));
});
}).unwrap().then(QtFuture::Launch::Async, [printer](){
printer->print("Application work finished [success]");
}).onCanceled([printer] {
printer->print("Application work finished [cancel]");
}).onFailed([printer]{
printer->print("Application work finished [fail]");
});
![](https://habrastorage.org/getpro/habr/upload_files/721/822/5f0/7218225f0247766aa5703470206bde1a.png)
Но при этом запрос может быть выполнен из абсолютно любого потока, continuation может быть также обработан абсолютно любым потоком. Использовать QtFuture::Launch::Async тоже можно, что даёт достаточно большой запас гибкости.
Расширение
А что если мы захотим, помимо get, делать post/put или любые другие запросы? Без проблем. Нужно лишь расширить HttpManager. В качестве примера, добавим поддержку post-запросов.
Для начала создадим dto для передачи post-запроса между потоками
HttpPostRequestEvent
class HttpPostRequestEvent : public QEvent {
private:
QPromise<std::shared_ptr<QNetworkReply>> m_promise;
const QNetworkRequest m_request;
const QByteArray m_data;
public:
inline static constexpr QEvent::Type Type = static_cast<QEvent::Type>(QEvent::Type::User + 2);
HttpPostRequestEvent(const QNetworkRequest &request, const QByteArray &data);
public:
QPromise<std::shared_ptr<QNetworkReply>>& promise();
const QNetworkRequest& request() const;
const QByteArray& data() const;
};
HttpManager::HttpPostRequestEvent::HttpPostRequestEvent(const QNetworkRequest &request, const QByteArray &data)
:QEvent{ Type },
m_request{ std::move(request) },
m_data{ std::move(data) }
{
}
QPromise<std::shared_ptr<QNetworkReply> > &HttpManager::HttpPostRequestEvent::promise(){
return m_promise;
}
const QNetworkRequest &HttpManager::HttpPostRequestEvent::request() const{
return m_request;
}
const QByteArray &HttpManager::HttpPostRequestEvent::data() const{
return m_data;
}
Класс практически повторяет HttpGetRequestEvent, но дополнительно переносит QByteArray с параметрами.
Теперь добавим обработку этого события в метод customEvent
Метод customEvent
void HttpManager::customEvent(QEvent *event) {
std::shared_ptr<QNetworkReply> reply{};
QPromise<std::shared_ptr<QNetworkReply>> requestPromise{};
if(auto request = dynamic_cast<HttpGetRequestEvent*>(event); request) {
reply = std::shared_ptr<QNetworkReply>{ m_manager->get(request->request()), QScopedPointerDeleteLater{} };
requestPromise = std::move(request->promise());
} else if(auto request = dynamic_cast<HttpPostRequestEvent*>(event); request) {
reply = std::shared_ptr<QNetworkReply>{ m_manager->post(request->request(), request->data()), QScopedPointerDeleteLater{} };
requestPromise = std::move(request->promise());
}
QtFuture::connect(reply.get(), &QNetworkReply::finished).
then([reply, replyPromise = std::move(requestPromise)]() mutable ->void {
replyPromise.addResult(reply);
replyPromise.finish();
});
}
Сначала достаём из события promise и создаём запрос в зависимости от типа события, а затем делаем QtFuture::connect, в котором заполним promise.
Ну и последним шагом останется добавить в public-секцию HttpManager метод post, принимающий параметры запроса, и возвращающий QFuture на shared_ptr<QNetworkReply>.
Метод post
//HttpManager.h
QFuture<std::shared_ptr<QNetworkReply>> post(const QNetworkRequest& request, const QByteArray &data);
//HttpManager.cpp
QFuture<std::shared_ptr<QNetworkReply>> HttpManager::post(const QNetworkRequest &request, const QByteArray &data) {
auto task = new HttpPostRequestEvent{ std::move(request), std::move(data) };
auto future = task->promise().future();
qApp->postEvent(this, task);
return future;
}
Код практически повторяет код метода get. Меняется лишь создаваемое событие.
Попробуем использовать новый метод. В качестве теста кинет post-запрос на страницу Хабра со второй частью цикла.
Использование
auto printer = std::make_shared<EventBasedAsyncQDebugPrinter>();
auto httpManager = std::make_shared<HttpManager>();
auto printerThread = new QThread{};
printer->moveToThread(printerThread);
printerThread->start();
auto httpThread = new QThread{};
httpManager->moveToThread(httpThread);
httpThread->start();
httpManager->post(QNetworkRequest{ QUrl{ "https://habr.com/ru/post/710368/" } }, {}).
then(QtFuture::Launch::Async, [printer](std::shared_ptr<QNetworkReply> reply) {
printer->print(reply->readAll());
});
Хабр не позволяет post-запросы к постам, поэтому получим ошибку. Из этой ошибки мы видим, что post запрос всё-таки был проведён. А больше нам и не надо.
![](https://habrastorage.org/getpro/habr/upload_files/b8c/28b/602/b8c28b60276e453530bcb9d1398d183a.png)
Подобным образом можно расширить HttpManager любыми нужными вам методами.
Заключение
Эта статья вышла самой длинной в цикле, в ней была куча кода, как рабочего, так и не очень. Код, как обычно, на GitHub.
Если есть желание увидеть реализацию ещё каких-то active object — пишите в личку.