让人做网站需要准备什么条件,珠海网站,wordpress文本组件使用方法,营销咨询顾问本文使用 Zhihu On VSCode 创作并发布本文使用 CC BY-NC-SA 4.0 许可协议#xff0c;转载请注明来源一、设计背景众所周知#xff0c;Qt 的信号槽系统提供了线程安全的跨线程异步执行代码的机制(Qt::QueuedConnection)。使用该机制#xff0c;可以让槽函数代码在另一个线程执…本文使用 Zhihu On VSCode 创作并发布本文使用 CC BY-NC-SA 4.0 许可协议转载请注明来源一、设计背景众所周知Qt 的信号槽系统提供了线程安全的跨线程异步执行代码的机制(Qt::QueuedConnection)。使用该机制可以让槽函数代码在另一个线程执行并且可以携带参数用户代码无需加锁只要发射信号即可。但很多时候我们仅仅只想单次异步执行一段代码。若是通过信号槽机制执行则就不得不声明一个信号函数连接信号槽再发射信号这样显然很繁琐。幸好Qt 本身也知道这种需求的存在提供了 QTimer::singleShot() 函数可以跨线程异步执行槽函数甚至还可以延迟执行——然而该函数只能执行无参数槽函数不能执行其它类型的回调如 lambda。所以最好能够有一个类似 QTimer::singleShot()但又可以接收任意参数个数的任意函数子的 API。更新5.3的老代码写太久思维定势了刚查了下5.4的 singleShot 是支持 Functor 的……那这篇文章留作该机制的技术探讨吧……更新2Qt 5.4之后的 QTimer::singleShot 实现有坑有一个 Qt 事件循环机制理论上不应该出现的问题详见文末更新。考虑到异步执行时对执行结果的访问可以参考 std::async()返回一个 future 对象。但不能直接使用 std::future——因为它的 get 和 wait 会阻塞住线程对于 Qt 而言就会阻塞事件循环。即我们还需要一个不会阻塞事件循环的等待机制。综上所述需求总结如下提供跨线程异步执行代码的能力让回调函数在目标线程执行提供对任意函数子的异步执行接口可以接受具备任意参数个数的任意函数子提供延迟执行功能以满足 QTimer::singleShot() 的所有功能便于替代前者提供 future 返回对象用于处理返回值和等待同步接口与 std::future 类似提供不阻塞 Qt 事件循环的等待机制用于供 future 使用。二、异步回调实现跨线程异步回调的实现可以参考 Qt 的元对象机制。Qt 通过元对象系统进行异步执行时信号槽、QTimer::singleShot()、QMetaMethod::invoke 等本质上是将回调函数封装为 QMetaCallEvent 对象再通过 QCoreApplication::postEvent() 投送至目标对象。目标对象会在所属线程的事件循环中触发 QObject::event() 事件处理函数解析事件并执行回调函数。然而 QMetaCallEvent 是非公开接口Qt 不保证其接口的可用和稳定性因此我们需要仿照此流程自行封装。2.1 异步回调事件类新建一个事件类继承自 QEvent并注册获取事件类型编号class AsyncInvokeEvent : public QEvent {public:static const int kEventType;std::functionQVariant(void) Function;std::promiseQVariant;std::shared_futureQVariant;
};
const int AsyncInvokeEvent::kEventType QEvent::registerEventType();
AsyncInvokeEvent::AsyncInvokeEvent() : QEvent(QEvent::Type(kEventType)) {}
将用户通过 API 传入的回调函数封装为 std::functionQVariant(void) 对象以擦除类型信息便于封入事件类中。考虑到需要获取返回值此处使用 Qt 的万能动态类型 QVariant 存储返回类型但代价是返回值必须注册至 Qt 元对象系统——也可将 future 实现为模板类型但这会导致代码复杂度大幅增加并且不得不将 cpp 中的大部分流程暴露至头文件。2.2 异步事件过滤器将异步回调事件发送至目标线程时需要有一个重写了 QObject::event() 函数的对象接受该事件。我们可以考虑为每个 Qt 线程建立一个事件过滤器使用一个全局的字典保存在使用时通过线程指针查询该字典若未检索到则新建之即惰性初始化AsyncInvokerEventFilter* filter;
{// Find event filter for given threadstatic std::atomic_flag flag ATOMIC_FLAG_INIT;static QHashQThread*, AsyncInvokerEventFilter* filters;while (flag.test_and_set(std::memory_order_seq_cst)) { // Spin-lock}auto it filters.find(thread);if (it filters.end()) {it filters.insert(thread, new AsyncInvokerEventFilter{thread});}filter *it;flag.clear(std::memory_order_release);
}
拿到事件过滤器后即可向其投送事件auto event new AsyncInvokeEvent;
event-Function function;
event-future event-promise.get_future();
QCoreApplication::postEvent(filter, event);
return event-future;
该事件会通过 Qt 的事件循环机制在目标线程中被传递至接收者的 event() 函数bool AsyncInvokerEventFilter::event(QEvent* event) {bool ret QObject::event(event);if (event-type() AsyncInvokeEvent::kEventType) {AsyncInvokeEvent* e static_castAsyncInvokeEvent*(event);e-Invoke();}event-accept();return ret;
}
至此跨线程异步执行代码的机制已经编写完毕整体其实是非常简单的。而且也并非 Qt 专属其实任意具备事件循环的框架都可以使用相同逻辑实现。2.3 生命周期控制Qt 信号槽的接收者指针除了指定槽函数执行的线程外还负责了生命周期控制的作用——只要 sender 或者 receiver 对象被析构则该信号槽便不会再执行。由于上文的异步回调事件类是由事件过滤器执行而非回调函数对应的逻辑意义上的接收者因此存在回调函数与其依赖资源的生命周期不一致的风险——我们需要引入额外的信息来监测回调函数的生命周期。虽然回调函数中也可以通过各类智能指针来管理资源的生命周期但这会强迫调用者编写更多的代码而且无法让事件在执行回调前判断相关资源生命周期是否已结束。因此我们需要一个机制来判断依赖资源的生命周期。由于在接口层可以做各式封装最终传递到执行点的判断方式可通过 std::functionbool(void) 来表达void AsyncInvokeEvent::Invoke() {QVariant ret;if (!IsAlive || IsAlive()) {ret Function();}promise.set_value(ret);
}
对外接口中可以考虑提供如下几种使用方式最基础的方式直接传递 std::functionbool(void) 回调函数可在其中封装各类自定义判断仿信号槽方式传递 QObject* 指针接口层通过 QPointer 类监测其存活状态并将其封装为回调函数无生命周期约束则接口层封装默认实现的回调函数自动返回 true。三、异步回调接口封装根据上文代码此机制的接口需要提供 (执行线程, 回调函数) 二元组作为输入参数以及一个可选参数 [生命周期判断回调]。为方便使用参考 Qt 的信号槽、 QTimer::singleShot() 语法也可直接提供一个 QObject* 对象指针作为逻辑意义上的接收者则可通过 QObject::thread() 函数获取执行线程。回调函数最终传递至内部实现的版本便是上文所述的 std::functionQVariant(void) 对象。但为方便使用我们可以提供 Func function, Args... args 形式的模板接口用于承接任意类型的函数子和函数参数template typename Func, typename... Args
AsyncInvoker::Future AsyncInvoker::Invoke(QThread* thread, const Func func,Args... args) {if (!thread) {thread qApp-thread();}auto f std::bind(func, std::forwardArgs(args)...);std::functionQVariant(void) function [f]{ return QVariant{f()}; };return Invoke(function, thread);
}
此处的封装返回值一句存在隐患因为传入函数有可能无返回值此时这行代码会无法编译。针对此情况我们可以去 Qt 源码中看看官方是如何处理的。顺着接收函数子作为槽函数的 QObject::connect() 源代码可在 qobjectdefs_impl.h 中找到如下黑魔法/*trick to set the return value of a slot that works even if the signal or the slot returns voidto be used like function(), ApplyReturnValueReturnType(return_value)if function() returns a value, the operator,(T, ApplyReturnValueReturnType) is called, but if itreturns void, the builtin one is used without an error.
*/
template typename T
struct ApplyReturnValue {void *data;explicit ApplyReturnValue(void *data_) : data(data_) {}
};
templatetypename T, typename U
void operator,(T value, const ApplyReturnValueU container) {if (container.data)*reinterpret_castU *(container.data) std::forwardT(value);}templatetypename T
void operator,(T, const ApplyReturnValuevoid ) {}
该模板类重载了逗号运算符然后再通过模板特化匹配到不同版本的实现对于有返回值的版本将返回值储存至构造时输入的对象指针中。仿写一下就能得到我们想要的了namespace impl {
template typename T
struct ApplyReturnValue {mutable QVariant* data_;explicit ApplyReturnValue(QVariant* data) : data_(data) {}
};
template typename T, typename U
inline void operator,(T value, const ApplyReturnValueU container) {container.data_-setValue(std::forwardT(value));
}
template typename T
inline void operator,(T, const ApplyReturnValuevoid) {}
} // namespace impltemplate typename Func, typename... Args
AsyncInvoker::Future AsyncInvoker::Invoke(QThread* thread, const Func func,Args... args) {if (!thread) {thread qApp-thread();}auto f std::bind(func, std::forwardArgs(args)...);std::functionQVariant(void) function [f] {using return_t decltype(func(std::forwardArgs(args)...));QVariant ret;f(), impl::ApplyReturnValuereturn_t(ret);return ret;};return Invoke(function, thread);
}
注意lambda 的返回类型无法通过 std::result_of 获取只能通过 decltype 获取。四、延迟执行延迟执行原理上也很简单将延迟事件一并封装入异步回调事件类中投送至事件过滤器后事件过滤器再启动一个定时器事件在定时器事件中才实际执行回调。考虑到性能问题此处不应为了执行一个回调函数就创建一个 QTimer 定时器对象并绑定信号槽。好消息是Qt 已经考虑到此类需求提供了一个轻量级的定时器接口 QObject::startTimer()无需额外新建任何对象以及信号槽。该接口会定时发起定时器事件通过 QObject::timerEvent 接收处理。因此将前文的 AsyncInvokerEventFilter::event() 代码进行改造如下// AsyncInvokeEvent 成员变量
// QSharedPointerAsyncInvokeData d;// AsyncInvokerEventFilter 成员变量
// QHashint, QSharedPointerAsyncInvokeData events_;bool AsyncInvokerEventFilter::event(QEvent* event) {bool ret QObject::event(event);if (event-type() AsyncInvokeEvent::kEventType) {AsyncInvokeEvent* e static_castAsyncInvokeEvent*(event);if (e-d-delay_ms 0) {// Deferred event, invoke in timerEventint id startTimer(e-d-delay_ms);events_[id] e-d;} else {e-d-Invoke();}}event-accept();return ret;
}void AsyncInvokerEventFilter::timerEvent(QTimerEvent* event) {int id event-timerId();killTimer(id);auto it events_.find(id);if (it events_.end()) {return;}it.value()-Invoke();events_.erase(it);
}
注意对于自定义事件无论 QObject::event() 返回是 true 还是 false或者通过 QEvent::accept() / QEvent::ignore() 接受或者忽略事件Qt 都会无视上述操作在执行完 QObject::event() 后直接删除由 QCoreApplication::postEvent() 投送的异步事件对象。因此对于需要延迟执行的事件直接将事件指针保存下来是无效的该指针会成为悬空指针。此处使用共享指针保存事件数据而非直接与容器内的值进行 std::swap()——因为这些数据在 Future 中也会被引用需要进行共享。此处不可使用 QCoreApplication::processEvents() 方式进行延时——因为若在延时过程中又接收到异步回调事件则会递归进入此函数以此类推存在多次递归导致爆栈的风险。五、Future 对象其实简单一点的话在异步回调事件类中存储一个 std::promise 对象然后返回它的 get_future() 即可。但前文也提到了std::future 等待操作会阻塞线程导致 Qt 事件循环失去响应因此我们需要编写一个不阻塞 Qt 事件循环的等待机制并且基于它来封装我们的 Future 类。5.1 不阻塞 Qt 事件循环的等待这个等待机制想必很多人都已经在自己的项目中广泛应用即使用计时器配合 QCoreApplication::processEvents() 实现不阻塞事件循环的延时QElapsedTimer timer;
timer.start()
while (timer.elapsed() timeout) {QCoreApplication::processEvents();
}
为方便定制化的使用我们可以参考 std::future 的 wait() / wait_for() / wait_until() 函数做多个额外的封装并提供 QDateTime 和 std::chrono 两套接口void Wait(const std::functionbool(void) isValid,QEventLoop::ProcessEventsFlags flags QEventLoop::AllEvents);bool WaitFor(int timeout_milliseconds, QEventLoop::ProcessEventsFlags flags QEventLoop::AllEvents,const std::functionbool(void) isValid {});bool WaitUntil(const QDateTime timeout_time,QEventLoop::ProcessEventsFlags flags QEventLoop::AllEvents,const std::functionbool(void) isValid {});template class Rep, class Period
bool WaitFor(const std::chrono::durationRep, Period timeout_duration,QEventLoop::ProcessEventsFlags flags QEventLoop::AllEvents,const std::functionbool(void) isValid {});template class Clock, class Duration
bool WaitUntil(const std::chrono::time_pointClock, Duration timeout_time,QEventLoop::ProcessEventsFlags flags QEventLoop::AllEvents,const std::functionbool(void) isValid {});
具体实现不再赘述本例思路如下WaitFor 中使用 当前时间 延时 方式转换为 WaitUntil 的调用。WaitUntil 中将超时判断封装为回调函数以转换为 Wait 的调用。5.2 Future 对象的 wait 与 getFuture 对象的 wait()/wait_for()/wait_until() 可直接调用上述实现。但 wait_for() / wait_until() 函数需要返回 std::future_status 状态值因此我们还需要判断该异步事件当前的执行状态。想必由于要避免阻塞事件循环我们不能直接调用 std::future 的对应函数因此需要自行封装执行状态。可考虑在异步回调事件类对象中存储一个 std::atomic_bool 标志位用于标识异步执行状态在回调执行后将其之为 true// Future 成员变量
// QSharedPointerAsyncInvokeData d_;std::future_status AsyncInvoker::Future::status() const {if (!d_-future.valid()) {return std::future_status::deferred;} else if (!d_-executed.load()) {return std::future_status::timeout;} else {return std::future_status::ready;}
}
则 wait_for() 和 wait_until() 函数在完成等待后返回 status() 即可wait() 则是将 status() 作为判断条件传给上一节的 Wait() 函数。get() 函数同理 status() 可以直接使用 wait() 完成等待然后返回 std::future::get() 即可。valid() 函数则是同时判断 std::future::valid() 和 executed 状态即 status() std::future_status::ready。六、范例代码上文中的代码已提交至 GitHub: ZgblKylin/KtUtils 仓库的 AsyncInvoker 分支。该仓库提供 CMake 和 QMake 两种使用方式支持静态链接和动态链接QMake 还提供源码包含。库文件会生成至 ${CMAKE_SOURCE_DIR}/lib 目录dll文件(特例)和单元测试的exe文件会生成至 ${CMAKE_SOURCE_DIR}/bin 目录库文件名称为 KtUtils/KtUtilsd(Debug 后缀)。CMake 使用方式# 启用动态链接。默认使用静态链接。
set(KT_UTILS_SHARED_LIBRARY ON)# 编译单元测试
set(BUILD_TESTING ON)# 链接目标
add_subdirectory(KtUtils)
target_link_libraries(TargetName KtUtils)单元测试使用 Qt Test 编写可使用 CMake 的 CTest 机制直接执行如 make test但该执行方式下无法看到 Qt Test 输出。QMake 使用方式# 源码包含
include(KtUtils/KtUtils.pri)# 链接库
# 修改 KtUtilsconf.pri 以启用动态链接、启用单元测试
SUBDIRS KtUtils
win32: {contains(KtUtils_CONFIG, KtUtils_Shared_Library) {LIBS -LKtUtils/bin/} else {LIBS -LKtUtils/lib/}
} else:unix: {LIBS -LKtUtils/lib/
}
CONFIG(release, debug|release): LIBS -lKtUtils
else:CONFIG(debug, debug|release): LIBS -lKtUtilsd
DESTDIR KtUtils/bin
INCLUDEPATH KtUtils/include七、QTimer::singleShot7.1 功能对比笔者之前写了5年的 Qt 5.3所以形成了一定的思维定势加上 Qt 极端注重兼容性基本不在大版本内做大更新所以忽略了某些问题……就是 Qt 5.4 其实算 breaking change只是不破坏老代码兼容性。5.4 开始API 设计全面提升到 C11了于是很多 API 都引入了 Functor 版本。5.4 的 QTimer::singleShot 加入了 FunctorArgs 的接口接口设计和功能与我文中的几乎一致。但我试用了发现有一个坑——无法在非 Qt 线程中调用 QTimer::singleShot此场景下该函数不会被执行。但 Qt 的事件循环机制是不应该有这问题的因为 Qt 的异步事件的处理底层为QCoreApplication::postEvent只取决于接收者的事件循环对发送者无任何要求。典型例子就是信号槽你可以在任何位置发信号甚至在类似中断的 catch 块、signal 函数回调等这些特殊位置发信号。那么 QTimer::singleShot 的这个问题是怎么出现的呢?这需要我们对比下两个方案的实现方式。7.2 问题分析我的方案人工仿造 QMetaCallEvent通过 QCoreApplication::postEvent 投递事件receiver 接收事件后再根据 timeout 参数来决定是否需要延时若需要则再通过 startTimer 转发至 timerEvent 事件。QTimer::singleShot 的方案该方案比较取巧把 invoke 和 timeout 两个动作合并到一起了然后比起我的方案还不需要给接收线程外挂一个 filter 处理器整体实现上的确更加优雅但也导致了此处的问题。建立一个 QSingleShotTimer 对象该对象本身承担了 invoke 功能同时继承自 QObject来一并处理延时功能直接在调用线程对该对象执行 startTimer 操作——因为此操作不能跨线程调用通过 moveToThread 将其移入接收者线程则已经启动的定时器会在该线程自动重新开启不用管了也不需要做啥 post把调用请求投送到另一个线程以及延迟执行都通过 moveToThread 这步一石二鸟了在 timerEvent 中直接 invoke 函数即可多么优雅。唯一纰漏在于非 Qt 线程无 Qt 事件循环的线程中无法启动定时器此时 moveToThread 做的“停止原线程中的定时器移动对象所有权到新线程后在新线程中自动注册定时器”的自动操作一开始就被堵死了。于是这个定时器永远跑不起来这个函数永远不会被执行。对了顺带还引发一个额外的副作用——如果你这个 functor 是捕获了变量的 lambda那么捕获的变量也就释放不掉了——也不是严格意义上的野指针化了因为在进程退出前还是会析构掉这个 QSingleShotTimer 对象的。7.3 替代方案那么为了避开这个坑难道我们就一定要重复造轮子了吗?也不是Qt 还是有一个老老实实走 QCoreApplication::postEvent 投递 QMetaCallEvent 的实现的。那就是 QMetaObject::invokeMethod。只是延迟执行功能就得自己造轮子了QMetaObject::invokeMethod(receiver, [timeout]{// 以下延时也可通过我前文封装的 WaitFor 函数实现auto start std::chrono::steady_clock::now();std::chrono::milliseconds duration{timeout};while (std::chrono::steady_clock::now() (start duration)) {QCoreApplication::processEvents();}...}, Qt::QueuedConnection);
怎么说呢?放着 QMetaCallEvent 的正道不走非要为了优雅玩花活结果玩出了一个本不应该有的坑…… 建议有异步延迟执行的需求时老老实实走最正统的 QMetaObject::invokeMethod 吧无非是封装个 WaitFor 方法多写一行代码来延时罢了。