Biblioteca de widgets asíncronos Qt-async

Buen dia a todos. Quiero hablar un poco sobre mi proyecto qt-async , tal vez parezca interesante o incluso útil para alguien.

La asincronía y el subprocesamiento múltiple se han incluido durante mucho tiempo en la vida cotidiana de los desarrolladores. Muchos lenguajes y bibliotecas modernos están diseñados teniendo en cuenta el uso asincrónico. El lenguaje C ++ también se mueve lentamente en esta dirección: han aparecido std :: thread, std :: promise / future, están a punto de incorporar rutinas y redes. La biblioteca Qt tampoco se queda atrás, ofreciendo sus análogos QThread, QRunnable, QThreadPool, QFuture, etc. Al mismo tiempo, no encontré widgets para mostrar acciones asincrónicas en Qt (tal vez estaba mirando mal, correcto si me equivoco).

Por lo tanto, decidí compensar la deficiencia e intentar implementar ese widget yo mismo. El desarrollo multiproceso es un negocio complejo pero interesante.

Antes de continuar con la implementación del widget, debe describir el modelo que presentará al usuario en forma de ventana. En su forma más general, el funcionamiento del widget me parece lo siguiente: en algún momento, el usuario o el sistema inicia una operación asincrónica. En este punto, el widget muestra el progreso de la operación o simplemente una indicación de la operación. Opcionalmente, el usuario tiene la capacidad de cancelar la operación. A continuación, la operación asincrónica se completa de dos maneras: se produjo un error y nuestro widget lo muestra, o el widget muestra el resultado de la operación exitosa.

Por lo tanto, nuestro modelo puede estar en uno de tres estados:

  1. Progreso: una operación asincrónica está en progreso
  2. Error: error en la operación asincrónica
  3. Valor: operación asincrónica completada con éxito

En cada uno de los estados, el modelo debe almacenar los datos correspondientes, así que llamé al modelo AsyncValue. Es importante tener en cuenta que la operación asincrónica en sí no es parte de nuestro modelo, solo cambia su estado. Resulta que AsyncValue se puede usar con cualquier biblioteca asincrónica, observando un patrón de uso simple:

  1. Al comienzo de la operación asincrónica, establezca AsuncValue en Progress
  2. Al final, ya sea en Error o en Valor, dependiendo del éxito de la operación
  3. Opcionalmente, durante la operación, puede actualizar los datos de Progreso y escuchar el indicador Detener si el usuario tiene la oportunidad de detener la operación.

Aquí hay un ejemplo esquemático usando 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; } 

El mismo esquema para trabajar con std :: thread:

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

Por lo tanto, la primera versión de nuestra clase podría verse así:

 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; }; 

Todos los que se encuentran con clases que admiten subprocesos múltiples saben que la interfaz de tales clases es diferente de los análogos de un solo subproceso. Por ejemplo, la función size () es inútil y peligrosa en un vector multiproceso. Su resultado puede volverse inmediatamente inválido, ya que el vector puede modificarse en este momento en otro hilo.

Los usuarios de la clase AsyncValue deberían poder acceder a los datos de la clase. Emitir una copia de los datos puede ser costoso, cualquiera de los tipos ValueType / ErrorType / ProgressType puede ser pesado. Emitir un enlace a datos internos es peligroso, en cualquier momento puede volverse inválido. Se propone la siguiente solución:

1. Dar acceso a los datos a través de las funciones accessValue / accessError / accessProgress, en las que se reciben las lambdas que reciben los datos correspondientes. Por ejemplo:

 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; } 

Por lo tanto, el acceso al valor interno se lleva a cabo por referencia y está bajo el bloqueo para lectura. Es decir, el enlace en el momento del acceso no se invalidará.

2. El usuario AsyncValue en la función accessValue puede recordar el enlace a los datos internos, siempre que esté suscrito a la señal stateChanged y después de procesar la señal ya no debe usar este enlace, porque ella se volverá inválida.

En tales condiciones, el consumidor de AsyncValue siempre tiene garantizado un acceso a datos válido y conveniente. Esta solución tiene varias consecuencias que afectan la implementación de la clase AsyncValue.

Primero, nuestra clase debe enviar una señal cuando cambia un estado, pero al mismo tiempo es una plantilla. Tendremos que agregar una clase Qt básica, donde podamos determinar la señal por la cual el widget actualizará su contenido, y todos los interesados ​​actualizarán los enlaces a los datos internos.

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

En segundo lugar, el momento de envío de la señal debe bloquearse para la lectura (de modo que AsyncValue no pueda cambiarse hasta que todos procesen la señal) y, lo más importante , en ese momento debe haber enlaces válidos a datos nuevos y antiguos. Porque en el proceso de enviar la señal, algunos consumidores de AsyncValue todavía usan los enlaces antiguos, y aquellos que procesaron la señal usan los nuevos.

Resulta que std :: variant no es adecuado para nosotros y tendremos que almacenar datos en la memoria dinámica para que las direcciones de los datos nuevos y antiguos no cambien.

Una pequeña digresión.

Puede considerar otras implementaciones de la clase AsyncValue que no requieren asignaciones dinámicas:

  1. Proporcione a los consumidores solo copias de los datos internos de AsyncValue. Como escribí anteriormente, tal solución puede ser más subóptima si los datos son grandes.
  2. Defina dos señales en lugar de una: stateWillChange / stateDidChange. Para obligar a los consumidores a deshacerse de los enlaces antiguos en la primera señal y recibir nuevos enlaces en la segunda señal. Me parece que este esquema complica excesivamente a los consumidores de AsyncValue, porque tienen intervalos de tiempo cuando se deniega el acceso a AsyncValue.

Se obtiene la siguiente implementación esquemática de la función setValue:

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

Como puede ver, necesitamos aumentar el bloqueo m_lock para escribir y devolverlo para leer. Desafortunadamente, no existe tal soporte en la clase QReadWriteLock. Puede lograr la funcionalidad deseada con un par de QMutex / QReadWriteLock. Aquí hay una implementación de la clase AsyncValue, casi real:

 //   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) }; 

Para aquellos que no están cansados ​​y no están perdidos, continuamos.

Como puede ver, tenemos funciones accessXXX que no esperan hasta que AsyncValue ingrese al estado correspondiente, sino que simplemente devuelven false. A veces es útil esperar sincrónicamente hasta que aparezca un valor o un error en AsyncValue. Esencialmente, necesitamos un análogo de std :: future :: get. Aquí está la firma de la función:

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

Para que esta función funcione, necesitamos una variable de condición: un objeto de sincronización que pueda esperarse en un subproceso y activarse en otro. En la función de espera, debemos esperar, y al cambiar el estado de AsyncValue de Progress a Value o Error, debemos notificar a los camareros.

Agregar otro campo a la clase AsyncValue, que es necesario en casos excepcionales cuando se utiliza la función de espera, me ha llevado a pensar: ¿puede este campo ser opcional? La respuesta es obvia, por supuesto, es posible si almacena std :: unique_ptr y lo crea si es necesario. La segunda pregunta surgió: ¿es posible hacer que este campo sea opcional y no hacer asignaciones dinámicas? A quién le importa, mira el siguiente código. La idea principal es la siguiente: la primera llamada de espera crea una estructura QWaitCondition en la pila y escribe su puntero en AsyncValue, las llamadas de espera posteriores simplemente verifican si el puntero no está vacío, use la estructura de este puntero, si el puntero está vacío, consulte la primera llamada de espera. .

 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)); } } 

Como ya se mencionó, AsyncValue no tiene un método para la computación asíncrona para no estar vinculado a una biblioteca específica. En cambio, se utilizan funciones gratuitas que implementan la asincronía de una forma u otra. El siguiente es un ejemplo de cálculo de AsyncValue en un grupo de subprocesos:

 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; } 

La biblioteca implementa otras dos funciones similares: asyncValueRunNetwork para procesar solicitudes de red y asyncValueRunThread, que realiza una operación en un subproceso recién creado. Los usuarios de la biblioteca pueden crear fácilmente sus propias funciones y usarlas allí para usar esas herramientas asincrónicas que usan en otros lugares.

Para aumentar la seguridad, la clase AsyncValue se ha extendido con otra clase de plantilla AsyncTrackErrorsPolicy, que le permite responder al mal uso de AsyncValue. Por ejemplo, aquí está la implementación predeterminada de la función AsyncTrackErrorsPolicy :: inProgressWhileDestruct, que se llamará si AsyncValue se destruye mientras se ejecuta la operación asincrónica:

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

En cuanto a los widgets, su implementación es bastante simple y concisa. AsyncWidget es un contenedor que contiene un widget para mostrar un error, progreso o un valor dependiendo del estado en que se encuentre actualmente AsyncValue.

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

El usuario está obligado a redefinir solo la primera función, para mostrar el valor, los otros dos tienen implementaciones predeterminadas.

La biblioteca qt-async resultó ser compacta, pero al mismo tiempo bastante útil. El uso de AsyncValue / AsyncWidget, donde anteriormente había funciones sincrónicas y una GUI estática, permitirá que sus aplicaciones se vuelvan modernas y más receptivas.

Para aquellos que han leído el bono hasta el final, un video de la aplicación de demostración

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


All Articles