Qt-asynchrone asynchrone Widget-Bibliothek

Guten Tag an alle. Ich möchte ein wenig über mein qt-async- Projekt sprechen, vielleicht erscheint es jemandem interessant oder sogar nützlich.

Asynchronität und Multithreading gehören seit langem ernsthaft zum Alltag der Entwickler. Viele moderne Sprachen und Bibliotheken sind für die asynchrone Verwendung konzipiert. Die C ++ - Sprache bewegt sich ebenfalls langsam in diese Richtung - std :: thread, std :: Versprechen / Zukunft sind erschienen, sie sind dabei, Coroutinen und Networking einzuführen. Die Qt-Bibliothek bleibt auch nicht zurück und bietet ihre Analoga QThread, QRunnable, QThreadPool, QFuture usw. an. Gleichzeitig habe ich in Qt keine Widgets zum Anzeigen asynchroner Aktionen gefunden (vielleicht habe ich schlecht ausgesehen, richtig, wenn ich mich irre).

Deshalb habe ich mich entschlossen, das Manko auszugleichen und zu versuchen, ein solches Widget selbst zu implementieren. Multithread-Entwicklung ist ein komplexes, aber interessantes Geschäft.

Bevor Sie mit der Implementierung des Widgets fortfahren, müssen Sie das Modell, das dem Benutzer angezeigt wird, in Form eines Fensters beschreiben. In seiner allgemeinsten Form scheint mir die Operation des Widgets wie folgt zu sein: Irgendwann startet der Benutzer oder das System eine asynchrone Operation. Zu diesem Zeitpunkt zeigt das Widget den Fortschritt des Vorgangs oder lediglich eine Anzeige des Vorgangs an. Optional kann der Benutzer den Vorgang abbrechen. Darüber hinaus wird die asynchrone Operation auf zwei Arten abgeschlossen: Entweder ist ein Fehler aufgetreten und unser Widget zeigt ihn an, oder das Widget zeigt das Ergebnis der erfolgreichen Operation an.

Somit kann sich unser Modell in einem von drei Zuständen befinden:

  1. Fortschritt - Eine asynchrone Operation wird ausgeführt
  2. Fehler - asynchroner Vorgang fehlgeschlagen
  3. Wert - Asynchrone Operation erfolgreich abgeschlossen

In jedem der Zustände muss das Modell die entsprechenden Daten speichern, daher habe ich das AsyncValue-Modell aufgerufen. Es ist wichtig zu beachten, dass die asynchrone Operation selbst nicht Teil unseres Modells ist, sondern nur ihren Status wechselt. Es stellt sich heraus, dass AsyncValue mit jeder asynchronen Bibliothek verwendet werden kann, wobei ein einfaches Verwendungsmuster zu beachten ist:

  1. Setzen Sie zu Beginn des asynchronen Vorgangs AsuncValue auf Progress
  2. Am Ende - entweder in Fehler oder in Wert, abhängig vom Erfolg der Operation
  3. Optional können Sie während des Vorgangs die Fortschrittsdaten aktualisieren und das Stopp-Flag abhören, wenn der Benutzer die Möglichkeit hat, den Vorgang zu stoppen.

Hier ist ein schematisches Beispiel mit QRunnable:

class MyRunnable : public QRunnable { public: MyRunnable(AsyncValue& value) : m_value(value) {} void run() final { m_value.setProgress(...); // do calculation if (success) m_value.setValue(...); else m_value.setError(...); } private: AsyncValue& m_value; } 

Das gleiche Schema für die Arbeit mit std :: thread:

 AsyncValue value; std::thread thread([&value] () { value.setProgress(...); // do calculation if (success) value.setValue(...); else value.setError(...); }); 

So könnte die erste Version unserer Klasse ungefähr so ​​aussehen:

 template <typename ValueType_t, typename ErrorType_t, typename ProgressType_t> class AsyncValue { public: using ValueType = ValueType_t; using ErrorType = ErrorType_t; using ProgressType = ProgressType_t; // public API private: QReadWriteLock m_lock; std::variant<ValueType, ErrorType, ProgressType> m_value; }; 

Jeder, der auf Klassen stößt, die Multithreading unterstützen, weiß, dass sich die Schnittstelle solcher Klassen von Single-Threaded-Analoga unterscheidet. Beispielsweise ist die Funktion size () in einem Multithread-Vektor nutzlos und gefährlich. Das Ergebnis kann sofort ungültig werden, da der Vektor momentan in einem anderen Thread geändert werden kann.

Benutzer der AsyncValue-Klasse sollten auf Klassendaten zugreifen können. Das Ausstellen einer Kopie der Daten kann teuer sein. Jeder der Typen ValueType / ErrorType / ProgressType kann schwer sein. Das Herstellen eines Links zu internen Daten ist gefährlich - es kann jederzeit ungültig werden. Die folgende Lösung wird vorgeschlagen:

1. Geben Sie über die Funktionen accessValue / accessError / accessProgress Zugriff auf Daten, in die Lambdas empfangen werden, die die entsprechenden Daten empfangen. Zum Beispiel:

 template <typename Pred> bool accessValue(Pred valuePred) { QReadLocker locker(&m_lock); if (m_value.index() != 0) return false; valuePred(std::get<0>(m_value)); return true; } 

Der Zugriff auf den internen Wert erfolgt somit als Referenz und ist zum Lesen unter Verschluss. Das heißt, der Link zum Zeitpunkt des Zugriffs wird nicht ungültig.

2. AsyncValue-Benutzer in der accessValue-Funktion können sich den Link zu internen Daten merken, vorausgesetzt, er hat das stateChanged-Signal abonniert und darf diesen Link nach der Verarbeitung des Signals nicht mehr verwenden, weil sie wird ungültig.

Unter solchen Bedingungen ist dem AsyncValue-Verbraucher immer ein gültiger und bequemer Datenzugriff garantiert. Diese Lösung hat mehrere Konsequenzen, die sich auf die Implementierung der AsyncValue-Klasse auswirken.

Zuerst sollte unsere Klasse ein Signal senden, wenn sich ein Zustand ändert, aber gleichzeitig ist es eine Vorlage. Wir müssen eine grundlegende Qt-Klasse hinzufügen, in der wir das Signal bestimmen können, mit dem das Widget seinen Inhalt aktualisiert, und alle Interessenten Links zu internen Daten aktualisieren.

 lass AsyncValueBase : public QObject { Q_OBJECT Q_DISABLE_COPY(AsyncValueBase) signals: void stateChanged(); }; 

Zweitens sollte der Moment des Sendens des Signals zum Lesen blockiert werden (so dass AsyncValue nicht geändert werden kann, bis alle das Signal verarbeitet haben), und vor allem sollte zu diesem Zeitpunkt gültige Links zu neuen und alten Daten vorhanden sein. Denn beim Senden des Signals verwenden einige AsyncValue-Konsumenten immer noch die alten Links, und diejenigen, die das Signal verarbeitet haben, verwenden die neuen.

Es stellt sich heraus, dass std :: variante für uns nicht geeignet ist und wir Daten im dynamischen Speicher speichern müssen, damit die Adressen neuer und alter Daten unverändert bleiben.

Ein kleiner Exkurs.

Sie können andere Implementierungen der AsyncValue-Klasse in Betracht ziehen, für die keine dynamischen Zuordnungen erforderlich sind:

  1. Geben Sie den Verbrauchern nur Kopien der internen Daten von AsyncValue. Wie ich bereits geschrieben habe, ist eine solche Lösung möglicherweise suboptimaler, wenn die Daten groß sind.
  2. Definieren Sie zwei Signale anstelle von einem: stateWillChange / stateDidChange. Verbraucher zu verpflichten, alte Verbindungen beim ersten Signal loszuwerden und beim zweiten Signal neue Verbindungen zu erhalten. Dieses Schema erschwert meines Erachtens die Verbraucher von AsyncValue übermäßig, weil Sie haben Zeitintervalle, in denen der Zugriff auf AsyncValue verweigert wird.

Die folgende schematische Implementierung der setValue-Funktion wird erhalten:

 void AsyncValue::setValue(...) {  m_lock            {   m_lock          m_lock   }  stateChanged       m_lock   }; 

Wie Sie sehen können, müssen wir die Sperre m_lock zum Schreiben erhöhen und zum Lesen zurückgeben. Leider gibt es in der QReadWriteLock-Klasse keine solche Unterstützung. Sie können die gewünschte Funktionalität mit einem Paar QMutex / QReadWriteLock erreichen. Hier ist eine Implementierung der AsyncValue-Klasse, die nahezu real ist:

 //   AsyncValue enum class ASYNC_VALUE_STATE { VALUE, ERROR, PROGRESS }; Q_DECLARE_METATYPE(ASYNC_VALUE_STATE); //        class AsyncValueBase : public QObject { Q_OBJECT Q_DISABLE_COPY(AsyncValueBase) signals: void stateChanged(ASYNC_VALUE_STATE state); protected: explicit AsyncValueBase(ASYNC_VALUE_STATE state, QObject* parent = nullptr); //     PromoteToWriteLock/DemoteToReadLock QMutex m_writeLock; QReadWriteLock m_contentLock; //   ASYNC_VALUE_STATE m_state; }; template <typename ValueType_t, typename ErrorType_t, typename ProgressType_t> class AsyncValueTemplate : public AsyncValueBase { //  struct Content { std::unique_ptr<ValueType_t> value; std::unique_ptr<ErrorType_t> error; std::unique_ptr<ProgressType+t> progress; }; Content m_content; public: using ValueType = ValueType_t; using ErrorType = ErrorType_t; using ProgressType = ProgressType_t; //    template <typename... Args> void emplaceValue(Args&& ...arguments) { moveValue(std::make_unique<ValueType>(std::forward<Args>(arguments)...)); } //    void moveValue(std::unique_ptr<ValueType> value) { //       Content oldContent; //   emplaceXXX/moveXXX    QMutexLocker writeLocker(&m_writeLock); { //       QWriteLocker locker(&m_contentLock); //      oldContent = std::move(m_content); //    m_content.value = std::move(value); //    m_state = ASYNC_VALUE_STATE::VALUE; //     } //   emitStateChanged(); //    emplaceXXX/moveXXX  //    } //   value void emplaceError(Args&& ...arguments); void moveError(std::unique_ptr<ErrorType> error); void emplaceProgress(Args&& ...arguments); void moveProgress(std::unique_ptr<ProgressType> progress); template <typename Pred> bool accessValue(Pred valuePred) { //     QReadLocker locker(&m_contentLock); //    if (m_state != ASYNC_VALUE_STATE::VALUE) return false; //      valuePred(*m_content.value); //     return true; } //  accessValue bool accessError(Pred errorPred) bool accessProgress(Pred progressPred) }; 

Für diejenigen, die nicht müde und nicht verloren sind, fahren wir fort.

Wie Sie sehen können, haben wir accessXXX-Funktionen, die nicht warten, bis AsyncValue in den entsprechenden Status wechselt, sondern einfach false zurückgeben. Manchmal ist es hilfreich, synchron zu warten, bis in AsyncValue entweder ein Wert oder ein Fehler angezeigt wird. Im Wesentlichen benötigen wir ein Analogon von std :: future :: get. Hier ist die Funktionssignatur:

 template <typename ValuePred, typename ErrorPred> void wait(ValuePred valuePred, ErrorPred errorPred); 

Damit diese Funktion funktioniert, benötigen wir eine Bedingungsvariable - ein Synchronisationsobjekt, das in einem Thread erwartet und in einem anderen aktiviert werden kann. In der Wartefunktion sollten wir warten, und wenn wir den Status von AsyncValue von Fortschritt auf Wert oder Fehler ändern, sollten wir die Kellner benachrichtigen.

Das Hinzufügen eines weiteren Felds zur AsyncValue-Klasse, das in seltenen Fällen erforderlich ist, wenn die Wartefunktion verwendet wird, hat mich zu der Überlegung veranlasst: Kann dieses Feld optional gemacht werden? Die Antwort liegt auf der Hand. Natürlich ist es möglich, wenn Sie std :: unique_ptr speichern und bei Bedarf erstellen. Die zweite Frage stellte sich: Ist es möglich, dieses Feld optional zu machen und keine dynamischen Zuordnungen vorzunehmen? Wen kümmert es, bitte schauen Sie sich den folgenden Code an. Die Hauptidee lautet wie folgt: Der erste Warteaufruf erstellt eine QWaitCondition-Struktur auf dem Stapel und schreibt seinen Zeiger auf AsyncValue. Nachfolgende Warteaufrufe prüfen einfach, ob der Zeiger nicht leer ist. Verwenden Sie die Struktur dieses Zeigers. Wenn der Zeiger leer ist, siehe oben für den ersten Warteaufruf .

 class AsyncValueBase : public QObject { ... struct Waiter { //     QWaitCondition waitValue; //   wait quint16 subWaiters = 0; //  wait     QWaitCondition waitSubWaiters; }; //    Waiter* m_waiter = nullptr; }; template <typename ValuePred, typename ErrorPred> void wait(ValuePred valuePred, ErrorPred errorPred) { //   -      if (access(valuePred, errorPred)) return; //  AsyncValue   QMutexLocker writeLocker(&m_writeLock); //     if (access(valuePred, errorPred)) return; //    wait  if (!m_waiter) { //  Waiter   Waiter theWaiter; //       if SCOPE_EXIT { //     wait, //    theWaiter if (m_waiter->subWaiters > 0) { //    subWaiters   do { m_waiter->waitSubWaiters.wait(&m_writeLock); } while (m_waiter->subWaiters != 0); } //   wait  , //       Waiter m_waiter = nullptr; }; //    Waiter  AsyncValue //    wait   m_waiter = &theWaiter; //   AsyncValue     Value  Error //    do { m_waiter->waitValue.wait(&m_writeLock); } while (!access(valuePred, errorPred)); } //   wait   else { //       else SCOPE_EXIT { //      m_waiter->subWaiters -= 1; //     ->   wait if (m_waiter->subWaiters == 0) m_waiter->waitSubWaiters.wakeAll(); }; //      m_waiter->subWaiters += 1; //   AsyncValue     Value  Error //    do { m_waiter->waitValue.wait(&m_writeLock); } while (!access(valuePred, errorPred)); } } 

Wie bereits erwähnt, verfügt AsyncValue nicht über eine Methode für asynchrones Rechnen, um nicht an eine bestimmte Bibliothek gebunden zu sein. Stattdessen werden freie Funktionen verwendet, die auf die eine oder andere Weise Asynchronität implementieren. Das folgende Beispiel zeigt die Berechnung von AsyncValue in einem Thread-Pool:

 template <typename AsyncValueType, typename Func, typename... ProgressArgs> bool asyncValueRunThreadPool(QThreadPool *pool, AsyncValueType& value, Func&& func, ProgressArgs&& ...progressArgs) { //    auto progress = std::make_unique<typename AsyncValueType::ProgressType>(std::forward<ProgressArgs>(progressArgs)...); //    auto progressPtr = progress.get(); //    AsyncValue if (!value.startProgress(std::move(progress))) return false; QtConcurrent::run(pool, [&value, progressPtr, func = std::forward<Func>(func)](){ SCOPE_EXIT { //     AsyncValue,    value.completeProgress(progressPtr); }; //  AsyncValue func(*progressPtr, value); }); return true; } 

Die Bibliothek implementiert zwei weitere ähnliche Funktionen: asyncValueRunNetwork zum Verarbeiten von Netzwerkanforderungen und asyncValueRunThread, das eine Operation für einen neu erstellten Thread ausführt. Bibliotheksbenutzer können auf einfache Weise ihre eigenen Funktionen erstellen und dort die asynchronen Tools verwenden, die sie an anderen Orten verwenden.

Um die Sicherheit zu erhöhen, wurde die AsyncValue-Klasse um eine weitere AsyncTrackErrorsPolicy-Vorlagenklasse erweitert, mit der Sie auf den Missbrauch von AsyncValue reagieren können. Hier ist beispielsweise die Standardimplementierung der Funktion AsyncTrackErrorsPolicy :: inProgressWhileDestruct, die aufgerufen wird, wenn AsyncValue während der Ausführung des asynchronen Vorgangs zerstört wird:

  void inProgressWhileDestruct() const { Q_ASSERT(false && "Destructing value while it's in progress"); } 

Die Implementierung von Widgets ist recht einfach und präzise. AsyncWidget ist ein Container, der ein Widget enthält, um einen Fehler oder Fortschritt oder einen Wert anzuzeigen, je nachdem, in welchem ​​Status sich AsyncValue derzeit befindet.

  virtual QWidget* createValueWidgetImpl(ValueType& value, QWidget* parent); virtual QWidget* createErrorWidgetImpl(ErrorType& error, QWidget* parent); virtual QWidget* createProgressWidgetImpl(ProgressType& progress, QWidget* parent); 

Der Benutzer ist verpflichtet, nur die erste Funktion neu zu definieren, um den Wert anzuzeigen, die anderen beiden haben Standardimplementierungen.

Die qt-async- Bibliothek erwies sich als kompakt, aber gleichzeitig sehr nützlich. Durch die Verwendung von AsyncValue / AsyncWidget, wo zuvor synchrone Funktionen und eine statische Benutzeroberfläche vorhanden waren, können Ihre Anwendungen moderner und reaktionsschneller werden.

Für diejenigen, die den Bonus bis zum Ende gelesen haben - ein Video der Demo-Anwendung

Source: https://habr.com/ru/post/de451722/


All Articles