• 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

  • 相关阅读:
    docker 创建新的镜像到私有仓库
    docker 数据管理<1>
    docker 数据管理<1>
    docker 运行挂载磁盘
    docker 运行挂载磁盘
    docker 容器管理上
    docker 指定容器名字
    消息队列应用场景解析
    Apache软件基金会Member陈亮:一名开源拓荒者的 Apache之旅
    【干货贴】消息队列如何利用标签实现消息过滤
  • 原文地址:https://www.cnblogs.com/findumars/p/10393324.html
Copyright © 2020-2023  润新知