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
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.
Не раскрыт один момент. Как после всей цепочке отдать результат в gui поток? Вот пример из статьи:
Допустим в методе show() мы вставляем изображение в лейбл. Но вот show же выполняется в другом не в gui потоке.
Чтобы show выполнялся в gui потоке, мне надо делать так?