mafulechka
mafulechka13. Oktober 2020 02:57

Asynchrone APIs in Qt 6

Wie die Leser vielleicht bereits wissen, bietet Qt mehrere Threading-Konstrukte (Threads, Mutexe, Wartezustände usw.) sowie APIs auf höherer Ebene wie QThreadPool, Qt Concurrent und andere verwandte Klassen. Dieser Artikel behandelt die asynchronen APIs auf höherer Ebene und die in Qt 6 eingeführten Änderungen.


Parallelitäts-API auf höherer Ebene in Qt

Qt Concurrent vereinfacht die Multithread-Programmierung, indem es die Notwendigkeit einer Synchronisierung auf niedriger Ebene (Primitive wie Mutexe und Sperren) eliminiert und mehrere Threads manuell verwaltet. Es bietet Matching-, Filter- und Reduktionsalgorithmen (besser bekannt aus der funktionalen Programmierung) für die parallele Verarbeitung sich wiederholender Container. Darüber hinaus gibt es Klassen wie QFuture, QFutureWatcher und QFutureSynchronizer für den Zugriff auf und die Überwachung der Ergebnisse asynchroner Berechnungen. Obwohl sie alle sehr nützlich sind, gab es immer noch einige Nachteile, wie die Unfähigkeit, QFuture außerhalb von Qt Concurrent zu verwenden, die fehlende Unterstützung für das Kombinieren mehrerer Berechnungen für einfacheren und saubereren Code, die mangelnde Flexibilität der Qt Concurrent API usw. Ab Qt 6 haben die Entwickler versucht, das in den letzten Jahren erhaltene Feedback zu berücksichtigen und die Multithread-Qt-Programmierung angenehmer und unterhaltsamer zu gestalten!

Fortsetzungen an QFuture anhängen

Ein gängiges Szenario bei der Multithread-Programmierung besteht darin, eine asynchrone Berechnung durchzuführen, die wiederum eine andere asynchrone Berechnung aufrufen und ihr Daten übergeben muss, die von der anderen abhängen, und so weiter. Da jede Phase die Ergebnisse der vorherigen erfordert, müssen Sie entweder warten (durch Blockieren oder Abfragen), bis die vorherige Phase abgeschlossen ist, und ihre Ergebnisse verwenden, oder Ihren Code in einem „Call-Callback“-Stil strukturieren. Keine dieser Optionen ist perfekt: Sie verschwenden entweder Ressourcen mit Warten oder erhalten komplexen, nicht wartbaren Code. Das Hinzufügen neuer Schritte oder Logik (zur Fehlerbehandlung usw.) erhöht die Komplexität weiter.

Betrachten Sie das folgende Beispiel, um das Problem besser zu verstehen. Angenommen, Sie möchten ein großes Bild aus dem Internet herunterladen, es umfassend verarbeiten und das resultierende Bild in Ihrer Anwendung anzeigen. Sie müssen also die folgenden Schritte ausführen:

• eine Netzwerkanfrage stellen und auf den Empfang aller Daten warten;
• ein Bild aus den Rohdaten erstellen;
• das Bild verarbeiten;
• ein Bild zeigen.

Es gibt folgende Methoden für jeden Schritt, die nacheinander aufgerufen werden müssen:

QByteArray download(const QUrl &url);
QImage createImage(const QByteArray &data);
QImage processImage(const QImage &image);
void show(const QImage &image);

Sie können QtConcurrent verwenden, um diese Aufgaben asynchron auszuführen, und QFutureWatcher, um den Fortschritt zu verfolgen:

void loadImage(const QUrl &url) {
    QFuture data = QtConcurrent::run(download, url);
    QFutureWatcher dataWatcher;
    dataWatcher.setFuture(data);

    connect(&dataWatcher, &QFutureWatcher ::finished, this, [=] {
        // handle possible errors
        // ...
        QImage image = createImage(data);
        // Process the image
        // ...
        QFuture processedImage = QtConcurrent::run(processImage, image);
        QFutureWatcher<QImage> imageWatcher;
        imageWatcher.setFuture(processedImage);

        connect(&imageWatcher, &QFutureWatcher::finished, this, [=] {
            // handle possible errors
            // ...
            show(processedImage);
        });
    });
}

Sieht nicht gut aus, oder? Die Logik der Anwendung wird mit dem Boilerplate-Code gemischt, der benötigt wird, um die Programmkomponenten miteinander zu verketten. Und Sie wissen, dass es umso gruseliger wird, je mehr Schritte Sie der Kette hinzufügen. QFuture hilft bei der Lösung dieses Problems, indem es Unterstützung für das Anhängen von Fortsetzungen über die Methode QFuture::then() hinzufügt:

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(show);

Es sieht auf jeden Fall viel besser aus! Aber eines fehlt - Fehlerbehandlung. Sie können Folgendes tun:

auto future = QtConcurrent::run(download, url)
            .then([](QByteArray data) {
                // handle possible errors from the previous step
                // ...
                return createImage(data);
            })    
            .then(...)    
            ...

Dies wird funktionieren, aber der Fehlerbehandlungscode ist immer noch mit der Programmlogik vermischt. Es lohnt sich wahrscheinlich auch nicht, die gesamte Kette auszuführen, wenn einer der Schritte fehlschlägt. Dies kann mit der Methode QFuture::onFailed() gelöst werden, mit der Sie für jeden möglichen Fehlertyp spezifische Fehlerhandler anhängen können:

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(show)
            .onFailed([](QNetworkReply::NetworkError) {
                // handle network errors
            })
            .onFailed([](ImageProcessingError) {
                // handle image processing errors
            })
            .onFailed([] {
                // handle any other error
            });

Beachten Sie, dass Ausnahmen aktiviert werden müssen, um .onFailed() verwenden zu können. Wenn einer der Schritte mit einer Ausnahme fehlschlägt, wird die Kette unterbrochen und der Fehlerhandler, der dem Typ der ausgelösten Ausnahme entspricht, wird aufgerufen.

Wie .then() und onFailed() gibt es auch .onCanceled(), um Handler anzuhängen, falls das Future abgebrochen wird.

Eine QFuture aus Signalen erstellen

Wie Futures sind auch Signale etwas, das irgendwann in der Zukunft verfügbar sein wird, daher erscheint es natürlich, mit ihnen wie Futures arbeiten zu können, Fortsetzungen anzufügen, Fehlerbehandler usw. Gegeben sei eine QObject-basierte MyObject-Klasse mit einem void-Signal mySignal (int), können Sie dieses Signal wie folgt als Future verwenden:

QFuture intFuture = QtFuture::connect(&object, &MyObject::mySignal);

Sie können jetzt Handler für Continue, Fail oder Cancel an das empfangene Future anhängen.

Beachten Sie, dass der Typ des resultierenden Future derselbe ist wie der Typ des Signalarguments. Wenn es keine Argumente hat, wird ein QFuture zurückgegeben . Bei mehreren Argumenten wird das Ergebnis in einem std::tuple gespeichert.

Kehren wir zum ersten Schritt (d. h. Laden) Ihres Bildverarbeitungsbeispiels zurück, um zu sehen, wie dies in der Praxis nützlich sein kann. Es gibt viele Möglichkeiten, dies zu implementieren. Verwenden wir QNetworkAccessManager, um eine Netzwerkanforderung zu senden und Daten abzurufen:

QNetworkAccessManager manager;    
...

QByteArray download(const QUrl &url) {        
    QNetworkReply *reply = manager.get(QNetworkRequest(url));
    QObject::connect(reply, &QNetworkReply::finished, [reply] {...});

    // wait until we've received all data
    // ...    
    return data;        
}

Aber das Warten auf die Sperre oben ist nicht gut, es wäre besser, wenn wir das loswerden und stattdessen sagen könnten: "Wenn QNetworkAccessManager die Daten erhält, erstelle ein Bild, verarbeite es und zeige dann" Bild, dann verarbeite es und zeige es an" ). Sie können dies tun, indem Sie das Signal „finished()“ des Netzwerkzugriffsmanagers mit QFuture verbinden:

QNetworkReply *reply = manager.get(QNetworkRequest(url));

auto future = QtFuture::connect(reply, &QNetworkReply::finished)
        .then([reply] {
            return reply->readAll();
        })
        .then(QtFuture::Launch::Async, createImage)
        .then(processImage)
        .then(show)        
        ...

Sie werden vielleicht bemerken, dass wir, anstatt QtConcurrent::run() zum asynchronen Laden und Zurückgeben von Daten in einem neuen Thread zu verwenden, einfach eine Verbindung zum Signal QNetworkAccessManager::finished() herstellen, das die Berechnungskette startet. Beachten Sie auch den zusätzlichen Parameter in der nächsten Zeile:

 .then(QtFuture::Launch::Async, createImage)

Standardmäßig werden mit .then() angehängte Fortsetzungen in demselben Thread aufgerufen, in dem der übergeordnete Thread ausgeführt wurde (in Ihrem Fall der Hauptthread). Da wir nun QtConcurrent::run() nicht verwenden, um die Kette asynchron zu starten, müssen wir einen zusätzlichen Parameter an QtFuture::Launch::Async übergeben, um die Fortsetzungskette in einem separaten Thread auszuführen und das Blockieren der Benutzeroberfläche zu vermeiden.

Erstellen einer QFuture

Bisher war die einzige "offizielle" Möglichkeit, einen Wert in einem QFuture zu erstellen und zu speichern, die Verwendung eines der QtConcurrent. Außerhalb von QtConcurrent war QFuture also nicht sehr nützlich. Qt 6 hat endlich ein "Setter"-Analogon von QFuture: QPromise, eingeführt von Andrey Golubev. Es kann verwendet werden, um Werte, Fortschritt und Ausnahmen für asynchrone Berechnungen festzulegen, auf die später über QFuture zugegriffen werden kann. Um zu demonstrieren, wie das funktioniert, schreiben wir das Bildverarbeitungsbeispiel noch einmal um und verwenden die QPromise-Klasse:

QFuture download(const QUrl &url) {
    QPromise promise;
    QFuture future = promise.future();

    promise.reportStarted(); // notify that download is started

    QNetworkReply *reply = manager.get(QNetworkRequest(url));
    QObject::connect(reply, &QNetworkReply::finished,
            [reply, p = std::move(promise)] {
                p.addResult(reply->readAll());
                p.reportFinished(); // notify that download is finished
                reply->deleteLater();
            });

    return future;
}
auto future = download()
        .then(QtFuture::Launch::Async, createImage)
        .then(processImage)
        .then(show)
        ...

Änderungen in QtConcurrent

Dank Jarek Kobus, Marten Nordheim, Carsten Heimrich, Timur Pocheptsov und Vitaly Fanaskov hat auch QtConcurrent einige nette Updates erhalten. Die bestehenden APIs wurden verbessert, insbesondere:

  • Sie können jetzt Ihren eigenen Thread-Pool für alle QtConcurrent-Methoden festlegen, anstatt sie immer im globalen Thread-Pool auszuführen und möglicherweise die Ausführung anderer Aufgaben zu blockieren.
  • Map-Objektreduktions- und Filteralgorithmen können jetzt einen Anfangswert annehmen, sodass Sie nicht nach Problemumgehungen für Typen suchen müssen, die keinen Standardkonstruktor haben.
  • QtConcurrent::run wurde verbessert, um mit variadischen Argumenten und Move-Only-Typen zu arbeiten.

Darüber hinaus wurden QtConcurrent zwei neue APIs hinzugefügt, um den Benutzern mehr Flexibilität zu bieten. Sehen wir sie uns genauer an.

QtConcurrent::runWithPromise

Die neue QtConcurrent::runWithPromise()-Methode, die von Jarek Kobus entwickelt wurde, ist eine weitere nette Ergänzung des QtConcurrent-Frameworks. Es ist QtConcurrent::run() sehr ähnlich, außer dass es dem Benutzer das mit der gegebenen Aufgabe verknüpfte QPromise-Objekt zur Verfügung stellt. Dies wird erreicht, indem das Runnable an QtConcurrent::runWithPromise() übergeben wird und einen Verweis auf das QPromise-Objekt nimmt:

auto future = QtConcurrent::runWithPromise(
            [] (QPromise &promise, /* other arguments may follow */ ...) {
                // ...
                for (auto value : listOfValues) {
                    if (promise.isCanceled())
                        // handle the cancellation

                // do some processing...

                promise.addResult(...);
                promise.setProgressValue(...);
                }
            },
            /* pass other arguments */ ...);

Wie Sie sehen können, haben Benutzer mit runWithPromise() mehr Kontrolle über die Aufgabe und können auf Anfragen zum Abbrechen oder Anhalten reagieren, den Fortschritt melden usw., was mit QtConcurrent::run() nicht möglich ist.

QtConcurrent::task

QtConcurrent::task() bietet eine lose Schnittstelle zum Ausführen einer Aufgabe in einem separaten Thread. Dies ist eine modernere Alternative zu QtConcurrent::run(), mit der Sie Aufgaben bequemer einrichten können. Anstatt einen der wenigen QtConcurrent::run()-Reloads zu verwenden, um die Parameter zum Ausführen der Aufgabe zu übergeben, können Sie sie in beliebiger Reihenfolge angeben, nicht benötigte überspringen und so weiter. Zum Beispiel:

QFuture future = QtConcurrent::task(doSomething)
        .withArguments(1, 2, 3)
        .onThreadPool(pool)
        .withPriority(10)
        .spawn();

Beachten Sie, dass Sie im Gegensatz zu run() auch eine Aufgabenpriorität übergeben können.

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Stabiles Hosting des sozialen Netzwerks EVILEG. Wir empfehlen VDS-Hosting für Django-Projekte.

Magst du es? In sozialen Netzwerken teilen!

M
  • 17. Februar 2022 08:22

Не раскрыт один момент. Как после всей цепочке отдать результат в gui поток? Вот пример из статьи:

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(show);

Допустим в методе show() мы вставляем изображение в лейбл. Но вот show же выполняется в другом не в gui потоке.
Чтобы show выполнялся в gui потоке, мне надо делать так?

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(QtFuture::Launch::Sync, show);

Kommentare

Nur autorisierte Benutzer können Kommentare posten.
Bitte Anmelden oder Registrieren
Letzte Kommentare
ИМ
Игорь Максимов5. Oktober 2024 07:51
Django – Lektion 064. So schreiben Sie eine Python-Markdown-Erweiterung Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55. Juli 2024 11:02
QML - Lektion 016. SQLite-Datenbank und das Arbeiten damit in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssr8. Februar 2024 18:43
Qt Linux - Lektion 001. Autorun Qt-Anwendung unter Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lektion 007. Arbeiten mit ICMP-Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25. Dezember 2023 10:30
Boost - statisches Verknüpfen im CMake-Projekt unter Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
Jetzt im Forum diskutieren
J
JacobFib17. Oktober 2024 03:27
добавить qlineseries в функции Пользователь может получить любые разъяснения по интересующим вопросам, касающимся обработки его персональных данных, обратившись к Оператору с помощью электронной почты https://topdecorpro.ru…
JW
Jhon Wick1. Oktober 2024 15:52
Indian Food Restaurant In Columbus OH| Layla’s Kitchen Indian Restaurant If you're looking for a truly authentic https://www.laylaskitchenrestaurantohio.com/ , Layla’s Kitchen Indian Restaurant is your go-to destination. Located at 6152 Cleveland Ave, Colu…
КГ
Кирилл Гусарев27. September 2024 09:09
Не запускается программа на Qt: точка входа в процедуру не найдена в библиотеке DLL Написал программу на C++ Qt в Qt Creator, сбилдил Release с помощью MinGW 64-bit, бинарнику напихал dll-ки с помощью windeployqt.exe. При попытке запуска моей сбилженной программы выдаёт три оши…
F
Fynjy22. Juli 2024 04:15
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …

Folgen Sie uns in sozialen Netzwerken