Qt 事件系统浅析 (用 Windows API 描述,分析了QCoreApplication::exec()和QEventLoop::exec的源码)(比起新号槽,事件机制是更高级的抽象,拥有更多特

事件系统在 Qt 中扮演了十分重要的角色,不只 GUI 的方方面面须要使用到事件系统,Signals/Slots 技术也离不开事件系统(多线程间)。咱们本文中暂且不描述 GUI 中的一些特殊状况,来讲说一个非 GUI 应用程序的事件模型。编程

若是让你写一个程序,打开一个套接字,接收一段字节而后输出,你会怎么作?windows

int main(int argc, char *argv[]) { WORD wVersionRequested; WSADATA wsaData; SOCKET sock; int err; BOOL bSuccess; wVersionRequested = MAKEWORD(2, 2); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) return 1; sock = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); if (sock == INVALID_SOCKET) return 1; bSuccess = WSAConnectByName(sock, const_cast<LPWSTR>(L"127.0.0.1"), ...); if (!bSuccess) return 1; WSARecv(sock, &wsaData, ...); WSACleanup(); return 0; } 

这就是所谓的阻塞模式。当 WSARecv 函数被调用后,线程将会被挂起,直到远程端有数据到达或某些系统中断被触发,程序自身将不能掌握控制权(除非使用 APC,详见 WSARecv function)。服务器

Qt 则提供了一个十分友好的编程模式 —— 事件驱动,其实事件驱动早已不是什么新鲜事,GUI 应用必然使用事件驱动,而愈来愈多服务器应用中也开始采用事件驱动模型(典型的有 Node.js 及其余采用 Reactor 模型的框架)。多线程

咱们举一个简单的事件驱动的例子,来看这样一段程序:app

int main(int argc, char *argv[]) { QApplication a(argc, argv); QTimer t; QObject::connect(&t, &QTimer::timeout, []() { qDebug() << "Timer fired!"; }); t.start(2000); return a.exec(); } 

你可能会问:“这跟 for-loop + sleep 的方式有什么区别?”嗯,从代码的层面确实不太好描述它们之间的区别。其实事件驱动与循环结构很是类似,由于它就是一个大循环,不断从消息队列中取出消息,而后再分发给事件响应者去处理。框架

因此一个消息循环能够用下面的伪代码来表示:异步

int main() { while (true) { Message msg = GetMessage(); if (msg.isQuitRequest) break; // Process the msg object... } // Clean up here... return 0; } 

看起来也很简单嘛,没错,大体结构就是这样,但实现细节倒是比较复杂的。函数

思考这样一个问题:CPU 处理消息的时间和消息产生的时间哪一个比较长?oop

按如今的 CPU 处理能力来说,消息处理是要远远快于消息产生的速度的,试想,你每秒能敲击几回键盘,手速再快 50 次了不起了吧,可是 CPU 每秒可以处理的敲击可能高达几万次。若是 CPU 处理完一个消息后,发现没的消息处理了,接下来可能很是多的 Cycle 后 CPU 仍然捞不着消息处理,这么多 Cycle 就白白浪费了。这就很是像 Mutex 和 Spin Lock 的关系,Spin Lock 只适用于很是短暂的互斥操做,操做时间一长,Spin Lock 就会严重消耗 CPU 资源, 由于它就是一个 while 循环,使用不断 CAS 尝试得到锁。post

回到咱们上面的消息列队,GetMessage 这个调用若是每次无论有没有消息都返回的话,CPU 就永远闲不下了,每一个线程始终 100% 的占用。这显然是不行的,因此 GetMessage 这个函数不会在没有消息时返回,相反,它会持续阻塞,直到有消息到达或者 timeout(若是指定了),这样以来 CPU 在没有消息的时候就能好好休息几千上万个 Cycle 了(线程挂起)。

Qt 的消息分发机制

好了,基本的原理了解了,咱们能够回来分析 Qt 了。为了弄明白上面 timer 的例子是怎么回事,咱们不妨在输出语句处加一个断点,看看它的调用栈:

QMetaObject 往上的部分已经不属于本文讨论的范围了,由于它属于 Qt 另外一大系统,即 Meta-Object System,咱们这里只分析到 QCoreApplication::sendEvent 的位置,由于一旦这个方法被调用了,再日后就没操做系统和事件机制什么事了。

首先咱们从一切的起点,QCoreApplication::exec 开始分析:

int QCoreApplication::exec() { if (!QCoreApplicationPrivate::checkInstance("exec")) return -1; QThreadData *threadData = self->d_func()->threadData; if (threadData != QThreadData::current()) { qWarning("%s::exec: Must be called from the main thread", self->metaObject()->className()); return -1; } if (!threadData->eventLoops.isEmpty()) { qWarning("QCoreApplication::exec: The event loop is already running"); return -1; } threadData->quitNow = false; QEventLoop eventLoop; self->d_func()->in_exec = true; self->d_func()->aboutToQuitEmitted = false; int returnCode = eventLoop.exec(); threadData->quitNow = false; if (self) self->d_func()->execCleanup(); return returnCode; } 

threadData 是一个 Thread-Local 变量,每一个线程都最多持有一个消息循环,这个方法主要作的就是启动主线程中的 QEventLoop。继续分析:

int QEventLoop::exec(ProcessEventsFlags flags)
{
    Q_D(QEventLoop);
    //we need to protect from race condition with QThread::exit
    QMutexLocker locker(&static_cast<QThreadPrivate *>(QObjectPrivate::get(d->threadData->thread))->mutex);
    if (d->threadData->quitNow)
        return -1;

    if (d->inExec) {
        qWarning("QEventLoop::exec: instance %p has already called exec()", this);
        return -1;
    }

    struct LoopReference {
        QEventLoopPrivate *d;
        QMutexLocker &locker;

        bool exceptionCaught;
        LoopReference(QEventLoopPrivate *d, QMutexLocker &locker) : d(d), locker(locker), exceptionCaught(true)
        {
            d->inExec = true;
            d->exit.storeRelease(false);
            ++d->threadData->loopLevel;
            d->threadData->eventLoops.push(d->q_func());
            locker.unlock();
        }

        ~LoopReference()
        {
            if (exceptionCaught) {
                qWarning("Qt has caught an exception thrown from an event handler. Throwing\n"
                         "exceptions from an event handler is not supported in Qt.\n"
                         "You must not let any exception whatsoever propagate through Qt code.\n"
                         "If that is not possible, in Qt 5 you must at least reimplement\n"
                         "QCoreApplication::notify() and catch all exceptions there.\n");
            }
            locker.relock();
            QEventLoop *eventLoop = d->threadData->eventLoops.pop();
            Q_ASSERT_X(eventLoop == d->q_func(), "QEventLoop::exec()", "internal error");
            Q_UNUSED(eventLoop); // --release warning
            d->inExec = false;
            --d->threadData->loopLevel;
        }
    };
    LoopReference ref(d, locker);

    // remove posted quit events when entering a new event loop
    QCoreApplication *app = QCoreApplication::instance();
    if (app && app->thread() == thread())
        QCoreApplication::removePostedEvents(app, QEvent::Quit);

    while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);

    ref.exceptionCaught = false;
    return d->returnCode.load();
}

这个方法是循环的主体,首先它处理了消息循环嵌套的问题,为何要嵌套呢?场景多是这样的:你想从一个模态窗口中获取一个用户的输入,而后继续逻辑的执行,若是模态窗口的显示是异步的,那编程模式就变成 CPS 了,用户输入将会触发一个 callback 进而完成接下来的任务,这在桌面开发中是不太可以被接受的(C# 玩家请绕行,大家有 await 了不得啊,摔)。若是用嵌套会是一种怎样的情景呢?须要开模态时再开一个新的 QEventLoop,因为 exec() 方法是阻塞的,在窗口关闭后 exit() 掉这个 event loop 就可让当前的方法继续执行了,同时你也拿到了用户的输入。QDialog 的模态就是这样作的。

Qt 这里使用内部 struct 来实现 try-catch-free 的风格,使用到的就是 C++ 的 RAII,非本文讨论范畴,不展开了。

再往下就是一个 while 循环了,在 exit() 方法执行以前,一直循环调用 processEvents() 方法。

processEvents 实现内部是平台相关的,Windows 使用的就是标准的 Windows 消息机制,macOS 上使用的是 CFRunLoop,UNIX 上则是 epoll。本文以 Windows 为例,因为该方法的代码量较大,本文中就不贴出完整源码了,你们能够本身查阅 Qt 源码。归纳地说这个方法大致作了如下几件事:

  1. 初始化一个不可见窗体(下文解释为何);
  2. 获取已经入队的用户输入或 Socket 事件;
  3. 若是 2 中没有获取到事件,则执行 PeekMessage,这个函数是非阻塞的,若是有事件则入队;
  4. 预处理 Posted Event 和 Timer Event;
  5. 处理退出消息;
  6. 若是上述步骤有一步拿到消息了,就使用 TranslateMessage(处理按键消息,将 KeyCode 转换为当前系统设置的相应的字符)+ DispatchMessage 分发消息;
  7. 若是没有拿到消息,那就阻塞着吧。注意,这里使用的是 MsgWaitForMultipleObjectsEx 这个函数,它除了能够监听窗体事件之外还能监听 APC 事件,比 GetMessage 要更通用一些。

下面来讲说为何要建立一个不可见窗体。建立过程以下:

static HWND qt_create_internal_window(const QEventDispatcherWin32 *eventDispatcher) { QWindowsMessageWindowClassContext *ctx = qWindowsMessageWindowClassContext(); if (!ctx->atom) return 0; HWND wnd = CreateWindow(ctx->className, // classname ctx->className, // window name 0, // style 0, 0, 0, 0, // geometry HWND_MESSAGE, // parent 0, // menu handle GetModuleHandle(0), // application 0); // windows creation data. if (!wnd) { qErrnoWarning("CreateWindow() for QEventDispatcherWin32 internal window failed"); return 0; } #ifdef GWLP_USERDATA SetWindowLongPtr(wnd, GWLP_USERDATA, (LONG_PTR)eventDispatcher); #else SetWindowLong(wnd, GWL_USERDATA, (LONG)eventDispatcher); #endif return wnd; } 

在 Windows 中,没有像 macOS 的 CFRunLoop 那样比较通用的消息循环,但当你有了一个窗体后,它就帮你在应用与操做系统之间创建了一个 bridge,经过这个窗体你就能够充分利用 Windows 的消息机制了,包括 Timer、异步 Winsock 操做等。同时 Windows API 也容许你绑定一些自定义指针,这样每一个窗体都与 event loop 创建了关系。

接下来 DispatchMessage 的调用会使窗体执行其绑定的 WindowProc 函数,这个函数分别处理 Socket、Notifier、Posted Event 和 Timer。

Posted Event 是一个比较常见的事件类型,它会进而触发下面的调用:

void QEventDispatcherWin32::sendPostedEvents() { Q_D(QEventDispatcherWin32); QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData); } 

在 QCoreApplicaton 中,sendPostedEvents() 方法会循环取出已入队的事件,这些事件被封装入 QPostEvent,真实的 QEvent 会被取出再传入 QCoreApplication::sendEvent() 方法,在此以后的过程就与操做系统无关了。

通常来讲,Signals/Slots 在同一线程下会直接调用 QCoreApplication::sendEvent() 传递消息,这样事件就能直接获得处理,没必要等待下一次 event loop。而处于不一样线程中的对象在 emit signals 以后,会经过 QCoreApplication::postEvent() 来发送消息:

void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority) { if (receiver == 0) { qWarning("QCoreApplication::postEvent: Unexpected null receiver"); delete event; return; } QThreadData * volatile * pdata = &receiver->d_func()->threadData; QThreadData *data = *pdata; if (!data) { delete event; return; } data->postEventList.mutex.lock(); while (data != *pdata) { data->postEventList.mutex.unlock(); data = *pdata; if (!data) { delete event; return; } data->postEventList.mutex.lock(); } QMutexUnlocker locker(&data->postEventList.mutex); if (receiver->d_func()->postedEvents && self && self->compressEvent(event, receiver, &data->postEventList)) { return; } if (event->type() == QEvent::DeferredDelete && data == QThreadData::current()) { int loopLevel = data->loopLevel; int scopeLevel = data->scopeLevel; if (scopeLevel == 0 && loopLevel != 0) scopeLevel = 1; static_cast<QDeferredDeleteEvent *>(event)->level = loopLevel + scopeLevel; } QScopedPointer<QEvent> eventDeleter(event); data->postEventList.addEvent(QPostEvent(receiver, event, priority)); eventDeleter.take(); event->posted = true; ++receiver->d_func()->postedEvents; data->canWait = false; locker.unlock(); QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire(); if (dispatcher) dispatcher->wakeUp(); } 

事件被加入列队,而后经过 QAbstractEventDispatcher::wakeUp() 方法唤醒正在被阻塞的 MsgWaitForMultipleObjectsEx 函数:

void QEventDispatcherWin32::wakeUp() { Q_D(QEventDispatcherWin32); d->serialNumber.ref(); if (d->internalHwnd && d->wakeUps.testAndSetAcquire(0, 1)) { // post a WM_QT_SENDPOSTEDEVENTS to this thread if there isn't one already pending PostMessage(d->internalHwnd, WM_QT_SENDPOSTEDEVENTS, 0, 0); } } 

唤醒的方法就是往这个线程所对应的窗体发消息。

 

以上就是 Qt 事件系统的一些底层的原理,虽然本文是相对 Windows 平台,但其余平台的实现也是有不少相通之处的,你们也能够自行研究一下。

 

了解了这些,咱们能够作什么呢?咱们能够轻松实现相似 Android 中 HandlerThread 那样的多线程模式。步骤就是:

  1. 建立一个 QThread;
  2. 将须要在新线程中使用的对象(需 QObject 子类,由于要用到 Signals/Slots)移入新线程(QObject::moveToThread());
  3. 使用 Signals/Slots 或 postEvent 触发对象中的方法。

 

以上。

  • Qt存在事件机制和信号槽机制,为何要有这两种机制?只是在不一样程度上去解耦以方便用户使用么

  • Cyandev (做者) 回复江江3 个月前
    事件机制是更高级的抽象,拥有更多特性,好比 accept/ignore,filter,仍是实现状态机等高级 API 的基础,而信号槽则是一切的基础,比较底层。

https://zhuanlan.zhihu.com/p/31402358

相关文章
相关标签/搜索