祝大家好。 我想谈谈我的
qt-async项目,也许对某人来说似乎很有意思甚至有用。
长期以来,异步和多线程已被认真地包含在开发人员的日常生活中。 许多现代语言和库在设计时都考虑了异步使用。 C ++语言也在朝着这个方向缓慢发展-std ::线程,std :: promise /未来已经出现,它们即将引入协程和网络。 Qt库也不会落后,它提供了类似的QThread,QRunnable,QThreadPool,QFuture等。 同时,我没有找到用于在Qt中显示异步操作的小部件(也许我看上去很糟,如果我弄错了,请更正)。
因此,我决定弥补这一缺点,并尝试自己实现这种小部件。 多线程开发是一项复杂但有趣的业务。
在继续执行小部件之前,您需要描述将以窗口形式呈现给用户的模型。 在其最一般的形式中,小部件的操作在我看来如下:在某个时间点,用户或系统启动异步操作。 此时,小部件将显示操作进度或仅显示操作指示。 用户可以选择取消操作。 接下来,以两种方式完成异步操作:发生错误并在我们的小部件中显示该错误,或者该小部件显示成功操作的结果。
因此,我们的模型可以处于以下三种状态之一:
- 进度-正在进行异步操作
- 错误-异步操作失败
- 值-异步操作成功完成
在每种状态下,模型都必须存储相应的数据,因此我将其称为AsyncValue模型。 重要的是要注意,异步操作本身不是我们模型的一部分,它只会切换其状态。 事实证明,AsyncValue可以与任何异步库一起使用,遵循一个简单的用法模式:
- 在异步操作开始时,将AsuncValue设置为Progress
- 最后-根据操作的成功,是错误还是价值
- (可选)在操作过程中,如果用户有机会停止操作,则可以更新“进度”数据并收听“停止”标志。
这是使用QRunnable的示意图示例:
class MyRunnable : public QRunnable { public: MyRunnable(AsyncValue& value) : m_value(value) {} void run() final { m_value.setProgress(...);
使用std :: thread的相同方案:
AsyncValue value; std::thread thread([&value] () { value.setProgress(...);
因此,该类的第一个版本可能如下所示:
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;
每个遇到支持多线程的类的人都知道,此类的接口与单线程类似物不同。 例如,在多线程向量中,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类的其他实现:
- 仅向消费者提供AsyncValue内部数据的副本。 如我之前所写,如果数据很大,则这种解决方案可能不是最佳选择。
- 定义两个信号而不是一个:stateWillChange / stateDidChange。 迫使消费者在第一个信号处摆脱旧的链接,而在第二个信号处接受新的链接。 在我看来,这种方案使AsyncValue使用者过于复杂,因为 当拒绝访问AsyncValue时,它们具有时间间隔。
获得setValue函数的以下示意性实现:
void AsyncValue::setValue(...) { m_lock { m_lock m_lock } stateChanged m_lock };
如您所见,我们需要增加用于写入的m_lock锁,并将其返回以进行读取。 不幸的是,QReadWriteLock类没有这种支持。 您可以使用一对QMutex / QReadWriteLock实现所需的功能。 这是AsyncValue类的实现,非常接近于实际:
对于那些不累也不迷路的人,我们继续。
如您所见,我们具有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 {
如前所述,AsyncValue没有用于异步计算的方法,因此不会绑定到特定的库。 而是使用自由函数以一种或另一种方式实现异步。 以下是在线程池上计算AsyncValue的示例:
template <typename AsyncValueType, typename Func, typename... ProgressArgs> bool asyncValueRunThreadPool(QThreadPool *pool, AsyncValueType& value, Func&& func, ProgressArgs&& ...progressArgs) {
该库还实现了两个类似的功能:用于处理网络请求的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,将使您的应用程序变得更现代且响应更快。
对于那些已阅读完奖金-视频演示应用程序的人