Qt 事件系统浅析 (用 Windows API 描述,分析了QCoreApplication::exec()和QEventLoop::exec的源码)(比起新号槽,事件机制是更高级的抽象,拥有更多特性,比如 accept/ignore,filter,还是实现状态机等高级 API 的基础)

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

如果让你写一个程序,打开一个套接字,接收一段字节然后输出,你会怎么做?

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 模型的框架)。

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

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 处理消息的时间和消息产生的时间哪个比较长?

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

回到我们上面的消息列队,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
"
                         "exceptions from an event handler is not supported in Qt.
"
                         "You must not let any exception whatsoever propagate through Qt code.
"
                         "If that is not possible, in Qt 5 you must at least reimplement
"
                         "QCoreApplication::notify() and catch all exceptions there.
");
            }
            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

原文地址:https://www.cnblogs.com/findumars/p/10393324.html