Ссылки на статьи

Предисловие

Во всех предыдущих статьях мы рассматривали лишь самый простой пример — последовательный вывод сообщений на экран в отдельном потоке.

Пришло время, наконец, сделать что-то более реальное и существенное, пусть и не очень сложное. И этим будет менеджер 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 им нет смысла.

Результат запуска нас огорчит.

Мы не можем сделать вызов метода 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 в список захвата лямбды, чтобы он не был удалён раньше времени.

Но и теперь у нас ничего не выйдет.

На этот раз всё сломалось из-за флага 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]");
});

И да. Это работает. Но мы имеем крайне мерзкую нежизнеспособную систему, в которой на каждый запрос придётся делать свой 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]");
});

Проблема в том, что мы всё равно создаём 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]");
});

Но при этом запрос может быть выполнен из абсолютно любого потока, 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 запрос всё-таки был проведён. А больше нам и не надо.

Подобным образом можно расширить HttpManager любыми нужными вам методами.

Заключение

Эта статья вышла самой длинной в цикле, в ней была куча кода, как рабочего, так и не очень. Код, как обычно, на GitHub.

Если есть желание увидеть реализацию ещё каких-то active object — пишите в личку.

Комментарии (0)