本文使用 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 的……那这篇文章留作该机制的技术探讨吧……
更新2:Qt 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::function<QVariant(void)> Function;
std::promise<QVariant>;
std::shared_future<QVariant>;
};
const int AsyncInvokeEvent::kEventType = QEvent::registerEventType();
AsyncInvokeEvent::AsyncInvokeEvent() : QEvent(QEvent::Type(kEventType)) {}
将用户通过 API 传入的回调函数封装为 std::function<QVariant(void)> 对象,以擦除类型信息,便于封入事件类中。
考虑到需要获取返回值,此处使用 Qt 的万能动态类型 QVariant 存储返回类型,但代价是返回值必须注册至 Qt 元对象系统——也可将 future
实现为模板类型,但这会导致代码复杂度大幅增加,并且不得不将 cpp
中的大部分流程暴露至头文件。
2.2 异步事件过滤器
将异步回调事件发送至目标线程时,需要有一个重写了 QObject::event() 函数的对象接受该事件。我们可以考虑为每个 Qt 线程建立一个事件过滤器,使用一个全局的字典保存,在使用时通过线程指针查询该字典,若未检索到则新建之,即惰性初始化:
AsyncInvokerEventFilter* filter;
{
// Find event filter for given thread
static std::atomic_flag flag = ATOMIC_FLAG_INIT;
static QHash<QThread*, 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 ev