Qt异步异步小部件库

祝大家好。 我想谈谈我的qt-async项目,也许对某人来说似乎很有意思甚至有用。

长期以来,异步和多线程已被认真地包含在开发人员的日常生活中。 许多现代语言和库在设计时都考虑了异步使用。 C ++语言也在朝着这个方向缓慢发展-std ::线程,std :: promise /未来已经出现,它们即将引入协程和网络。 Qt库也不会落后,它提供了类似的QThread,QRunnable,QThreadPool,QFuture等。 同时,我没有找到用于在Qt中显示异步操作的小部件(也许我看上去很糟,如果我弄错了,请更正)。

因此,我决定弥补这一缺点,并尝试自己实现这种小部件。 多线程开发是一项复杂但有趣的业务。

在继续执行小部件之前,您需要描述将以窗口形式呈现给用户的模型。 在其最一般的形式中,小部件的操作在我看来如下:在某个时间点,用户或系统启动异步操作。 此时,小部件将显示操作进度或仅显示操作指示。 用户可以选择取消操作。 接下来,以两种方式完成异步操作:发生错误并在我们的小部件中显示该错误,或​​者该小部件显示成功操作的结果。

因此,我们的模型可以处于以下三种状态之一:

  1. 进度-正在进行异步操作
  2. 错误-异步操作失败
  3. 值-异步操作成功完成

在每种状态下,模型都必须存储相应的数据,因此我将其称为AsyncValue模型。 重要的是要注意,异步操作本身不是我们模型的一部分,它只会切换其状态。 事实证明,AsyncValue可以与任何异步库一起使用,遵循一个简单的用法模式:

  1. 在异步操作开始时,将AsuncValue设置为Progress
  2. 最后-根据操作的成功,是错误还是价值
  3. (可选)在操作过程中,如果用户有机会停止操作,则可以更新“进度”数据并收听“停止”标志。

这是使用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; } 

使用std :: thread的相同方案:

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

因此,该类的第一个版本可能如下所示:

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

每个遇到支持多线程的类的人都知道,此类的接口与单线程类似物不同。 例如,在多线程向量中,size()函数是无用的且危险的。 由于向量可以在另一个线程中修改,因此其结果可能立即变得无效。

AsyncValue类的用户应该能够访问类数据。 发行数据的副本可能很昂贵,ValueType / ErrorType / ProgressType的任何类型都可能很繁重。 发布指向内部数据的链接很危险-在任何时候它都可能变得无效。 提出以下解决方案:

1.通过函数accessValue / accessError / accessProgress授予对数据的访问权限,在其中接收接收相应数据的lambda。 例如:

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

因此,内部值的访问是通过引用进行的,并且处于读取锁定的状态。 即,访问时的链接不会变为无效。

2. accessValue函数中的AsyncValue用户可以记住指向内部数据的链接,前提是该用户已订阅stateChanged信号,并且在处理该信号之后,该不再必须使用此链接,因为 她将变得无效。

在这种情况下,始终保证AsyncValue使用者具有有效且方便的数据访问。 此解决方案有几种影响AsyncValue类的实现的后果。

首先,当状态改变时,我们的类应该发送信号,但同时它是模板。 我们必须添加一个基本的Qt类,在这里我们可以确定小部件更新其内容的信号,所有感兴趣的人都将更新与内部数据的链接。

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

其次,应该阻止发送信号的时刻进行读取(以使AsyncValue直到每个人都处理完该信号后才能更改),并且最重要的是 ,那一刻应该存在指向新旧数据的有效链接。 因为在发送信号的过程中,某些AsyncValue使用者仍然使用旧的链接,而处理信号的人则使用新的链接。

事实证明std :: variant不适合我们,我们必须将数据存储在动态内存中,以便新旧数据的地址不变。

一个小题外话。

您可以考虑不需要动态分配的AsyncValue类的其他实现:

  1. 仅向消费者提供AsyncValue内部数据的副本。 如我之前所写,如果数据很大,则这种解决方案可能不是最佳选择。
  2. 定义两个信号而不是一个:stateWillChange / stateDidChange。 迫使消费者在第一个信号处摆脱旧的链接,而在第二个信号处接受新的链接。 在我看来,这种方案使AsyncValue使用者过于复杂,因为 当拒绝访问AsyncValue时,它们具有时间间隔。

获得setValue函数的以下示意性实现:

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

如您所见,我们需要增加用于写入的m_lock锁,并将其返回以进行读取。 不幸的是,QReadWriteLock类没有这种支持。 您可以使用一对QMutex / QReadWriteLock实现所需的功能。 这是AsyncValue类的实现,非常接近于实际:

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

对于那些不累也不迷路的人,我们继续。

如您所见,我们具有accessXXX函数,这些函数不会等到AsyncValue进入相应的状态,而只是返回false。 同步等待直到AsyncValue中出现值或错误有时会很有用。 实际上,我们需要一个std :: future :: get的类似物。 这是函数签名:

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

为了使此功能正常工作,我们需要一个条件变量-一个可以在一个线程中预期并在另一个线程中激活的同步对象。 在等待功能中,我们应该等待,当将AsyncValue的状态从“进度”更改为“值”或“错误”时,我们应该通知等待者。

向AsyncValue类添加另一个字段(在极少数情况下使用wait函数时是必需的)使我想到-该字段可以设为可选字段吗? 答案很明显,如果您存储std :: unique_ptr并在必要时创建它,当然是可能的。 出现第二个问题-是否可以使该字段为可选字段而不进行动态分配。 谁在乎,请看下面的代码。 主要思想如下:第一个等待调用在堆栈上创建一个QWaitCondition结构,并将其指针写入AsyncValue,随后的等待调用仅检查指针是否不为空,使用该指针的结构,如果指针为空,请参见上文中的第一个等待调用。

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

如前所述,AsyncValue没有用于异步计算的方法,因此不会绑定到特定的库。 而是使用自由函数以一种或另一种方式实现异步。 以下是在线程池上计算AsyncValue的示例:

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

该库还实现了两个类似的功能:用于处理网络请求的asyncValueRunNetwork和用于对新创建的线程执行操作的asyncValueRunThread。 库用户可以轻松创建自己的函数,并在那里使用它们在其他地方使用的那些异步工具。

为了提高安全性,已使用另一个AsyncTrackErrorsPolicy模板类扩展了AsyncValue类,该类允许您响应AsyncValue的滥用。 例如,这是AsyncTrackErrorsPolicy :: inProgressWhileDestruct函数的默认实现,如果在异步操作运行时销毁AsyncValue,则将调用该函数的默认实现:

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

至于小部件,它们的实现非常简单明了。 AsyncWidget是一个容器,其中包含一个小部件以显示错误或进度,或一个值,具体取决于当前AsyncValue处于何种状态。

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

用户只能重新定义第一个功能,以显示值,其他两个则具有默认实现。

事实证明, qt-async库很紧凑,但同时却非常有用。 使用以前具有同步功能和静态GUI的AsyncValue / AsyncWidget,将使您的应用程序变得更现代且响应更快。

对于那些已阅读完奖金-视频演示应用程序的人

Source: https://habr.com/ru/post/zh-CN451722/


All Articles