深入了解QT消息循环及线程相关性

发布于 10 天前  153 次阅读


前言

本篇文章作者为 WPS Windows上的一个开发大佬,经过他同意,发到我的平台上,来给大家分享

这里是作者链接!! https://www.zhihu.com/people/froser

下面为大佬的干货!

一、什么是Qt消息循环

Qt消息循环,就是从一个队列中不断取出消息,并响应消息的过程。窗体的鼠标、键盘、输入法、绘制,各种消息,都来自于Qt的消息循环。以Windows操作系统为例,Qt接管Windows原生窗口消息,并翻译成Qt的消息,派发给程序下的各个子对象、子QWidget等,通过接管层,可以很好屏蔽不同平台之间的差异性,开发人员不需要关心Windows或者X11的消息的差异性,只需要搞清楚各个QEvent之间是什么含义。

最开始的Qt消息循环开始于QCoreApplication::exec()。用户创建出一个QCoreApplication,或者说更多情况下是QApplication,执行QCoreApplication::exec(),一个应用程序便开始了。QCoreApplication会不断从操作系统获取消息,并且分发给QObject。

如果没有消息循环,那么Qt的信号和槽无法完全使用,有些函数也无法正确执行。举个例子,通过QueuedConnection连接的信号,其实是将一个事件压入了消息循环,如果没有QCoreApplication::exec(),那么这个消息循环将永远无法派发到指定的对象。

二、什么是线程相关性

准确点来说,应该是指QObject的线程相关性。以Qt文档中的示意图来作说明:

当我们创建一个QObject时,它会与创建自己所在的线程绑定。它参与的消息循环,其实是它所在线程的消息循环,如上图所示。假如某个线程没有默认的QThread::exec(),那么该线程上的QObject则无法接收到事件。另外,如果两个不同线程的QObject需要相互通信,那么只能通过QueuedConnection的方式,异步通知对方线程,在下一轮消息循环处理QObject的消息。

QObject的线程相关性默认会和它的parent保持一致。如果一个QObject没有parent,那么可以通过moveToThread,将它的线程相关性切换到指定线程。

了解QObject的线程相关性非常重要,很多初学者常常分不清一个多线程中哪些QObject应该由主线程创建,哪些应该由工作线程创建,我的观点是,它参与哪个消息循环,就由哪个来创建。

正因为这样的特性,我们才可以理解什么叫做AutoConnection。通过AutoConnect连接的两个QObject,如果是在同一个线程,那么可以直接调用(DirectConnection),如果不是在同一个线程,那么就通过事件通知的方式(QueuedConnection)来调用。通过信号和槽、事件或者QueuedConnection方式来进行线程间的通讯,尤其是与UI线程通讯,永远是最优雅的方式之一。

希望大家看了这篇文章后能有所帮助。

全文完。


等等,这就完了?这算哪门子深入????

好吧!以下才是正文!

真·深入了解QT消息循环及线程相关性

一、什么是消息循环

以Windows为例,在我们编写GUI程序,创建一个原生窗体时,总会要经历两个步骤:

1、注册一个窗体类(RegisterClassEx):

https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-registerclassexa

窗体类中,最重要的就是指定了一个窗口处理函数WNDPROC。所有此类型的窗口,收到事件后,都会回调到此处理函数中来执行。

2、创建一个窗体(CreateWindow)

https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-createwindowexw

一般地,我们可以创建很多个窗口,然后使用同一个窗体处理函数,通过创建时的参数来决定上下文,也就是到底是处理的是哪个Window,以及获取一些自定义结构等。这个函数大致定义了窗体的颜值,并且需要与第一步中的窗体类关联起来。这样一来,窗体就真正创建好了,并且也可以接收到系统发来的消息。

接下来很重要的一点,就是关于消息循环的过程。
首先,用户通过GetMessage、PeekMessage等函数,从消息队列中取出事件,接下来,通过DispatchMessage来分发事件。系统将这个事件分发到对应的窗口处理函数WNDPROC中进行处理。
在绝大部分GUI程序中,GetMessage, DispatchMessage是写在一个死循环中的,除非程序退出,否则会一直处理各种事件。

二、消息队列的线程相关性

依照MSDN的说法:

https://docs.microsoft.com/zh-cn/windows/win32/winmsg/about-messages-and-message-queues

系统将用创建某Window的线程来分发消息。例如窗体1在线程A创建,窗体2在线程B创建,那么它们的WNDPROC则是由不同线程来回调的。一般地我们也只会在主线程中创建窗体,不过系统还是允许在各个线程中处理窗口的。

三、Qt消息循环的基础:窗体事件

在Windows中,要处理事件一定要有一个窗体。在Qt中,事件一共有两类,一类是和窗体无关的实践,例如QTimerEvent,另外一类就是常见的窗体事件,如鼠标、键盘、绘制等事件。因此,qt至少有两个WNDPROC,一个处理Timer等事件,一个处理QWidget中的事件。

刚刚也提到,Windows事件其实是和线程相关的,那么也就是说,对于每一个QObject的对象,它必须要有自己所在线程的信息。不同线程的对象是无法直接通信的,要通过事件才可以。

在Qt中,消息循环在QEventLoop类中实现。通过QEventLoop::exec()可以进入一个消息循环的阻塞状态中,也就是不断地PeekMessage-DispatchMessage。其实,QEventLoop里面几乎没有实现任何细节,这可能有点令人迷惑,不过仔细想想,任何系统都可以通过QEventLoop来调用消息循环,说明里面一定有一层和系统相关的抽象,这个稍后会说到。

不难想到,QEventLoop通过内部的一层抽象,来不断从系统获取和处理消息,而这一层抽象,是和线程相关的。所有相同的线程,完全可以共用这层抽象。接下来就开始解析Qt4.8中对此的实现。

四、实现

1. QAbstractEventDispatcher

QAbstractEventDispatcher是一个处理PeekMessage-DispatchMessage的抽象接口。Windows上实现的派生类是QEventDispatcherWin32。QEventLoop从某个地方取到这个类的实例,来完成消息的获取和分发。

bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
    Q_D(QEventDispatcherWin32);

    if (!d->internalHwnd)
        createInternalHwnd();
    ...
    do {
        ....
        while (!d->interrupt) {
             ...
            MSG msg;
            bool haveMessage;

            if (!(flags & QEventLoop::ExcludeUserInputEvents) && !d->queuedUserInputEvents.isEmpty()) {
                ...
            } else {
                haveMessage = PeekMessage(&msg, 0, 0, 0, PM_REMOVE);
                ...
            }
            if (!haveMessage) {
                .....
            }
            if (haveMessage) {
                .....
                if (!filterEvent(&msg)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
                .....
            }
            retVal = true;
        }
    } while (canWait);
    .....
    return retVal;
}

以上是QEventDispatcherWin32的具体实现。我省略掉了大部分代码,只留下几个关键部分。
首先是看循环部分,其实就像原生的Windows程序那样,PeekMessage, TranslateMessage, DispatchMessage。我们调用QEventLoop::exec()后,便马上调进了这里。
第二个值得注意的是,

    if (!d->internalHwnd)
        createInternalHwnd();

createInternalHwnd(),QT用它创建了一个不可见的窗口,并且为它注册了一个叫做qt_internal_proc的WNDPROC函数:

LRESULT QT_WIN_CALLBACK qt_internal_proc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp)
{
    if (message == WM_NCCREATE)
        return true;

    MSG msg;
    msg.hwnd = hwnd;
    msg.message = message;
    msg.wParam = wp;
    msg.lParam = lp;
    QCoreApplication *app = QCoreApplication::instance();
    long result;
    if (!app) {
        if (message == WM_TIMER)
            KillTimer(hwnd, wp);
        return 0;
    } else if (app->filterEvent(&msg, &result)) {
        return result;
    }

#ifdef GWLP_USERDATA
    QEventDispatcherWin32 *q = (QEventDispatcherWin32 *) GetWindowLongPtr(hwnd, GWLP_USERDATA);
#else
    QEventDispatcherWin32 *q = (QEventDispatcherWin32 *) GetWindowLong(hwnd, GWL_USERDATA);
#endif
    QEventDispatcherWin32Private *d = 0;
    if (q != 0)
        d = q->d_func();

    if (message == WM_QT_SOCKETNOTIFIER) {
        // socket notifier message
        ...
        return 0;
    } else if (message == WM_QT_SENDPOSTEDEVENTS
        ...
        return 0;
    } else if (message == WM_TIMER) {
        Q_ASSERT(d != 0);
        d->sendTimerEvent(wp);
        return 0;
    }

    return DefWindowProc(hwnd, message, wp, lp);
}

可以看到,这个隐藏的窗口处理了几个事件。其中最常用的事件,肯定就是WM_TIMER了。通过QTimer::singleShot进来的事件,最终通过registerTimer()设置了计时器。

void QEventDispatcherWin32Private::registerTimer(WinTimerInfo *t)
{
    Q_ASSERT(internalHwnd);

    Q_Q(QEventDispatcherWin32);

    int ok = 0;
    if (t->interval > 20 || !t->interval || !qtimeSetEvent) {
        ok = 1;
        if (!t->interval)  // optimization for single-shot-zero-timer
            QCoreApplication::postEvent(q, new QZeroTimerEvent(t->timerId));
        else
            ok = SetTimer(internalHwnd, t->timerId, (uint) t->interval, 0);
    } else {
        ok = t->fastTimerId = qtimeSetEvent(t->interval, 1, qt_fast_timer_proc, (DWORD_PTR)t,
                                            TIME_CALLBACK_FUNCTION | TIME_PERIODIC | TIME_KILL_SYNCHRONOUS);
        if (ok == 0) { // fall back to normal timer if no more multimedia timers available
            ok = SetTimer(internalHwnd, t->timerId, (uint) t->interval, 0);
        }
    }

    if (ok == 0)
        qErrnoWarning("QEventDispatcherWin32::registerTimer: Failed to create a timer");
}

当SetTimer超时后,WM_TIMER将发送给internalHwnd,接下来它调用sendTimerEvent,通知接收的QObject,达到计时器的效果。

通过创建一个隐藏的窗口,来处理一些特定的事件,这便是Qt消息循环的一个小小的套路。

2. QThreadData

你可能会问,QEventDispatcherWin32的实例存放在哪里。前文也说过,QEventDispatcherWin32是跟着线程走的,所以没有必要每个QEventLoop都存一个。事实上,它存放在一个叫做QThreadData的结构中:

class QThreadData
{
    QAtomicInt _ref;

public:
    QThreadData(int initialRefCount = 1);
    ~QThreadData();

    static QThreadData *current();
    static QThreadData *get2(QThread *thread)
    { Q_ASSERT_X(thread != 0, "QThread", "internal error"); return thread->d_func()->data; }


    void ref();
    void deref();

    QThread *thread;
    bool quitNow;
    int loopLevel;
    QAbstractEventDispatcher *eventDispatcher;
    QStack<QEventLoop *> eventLoops;
    QPostEventList postEventList;
    bool canWait;
    QVector<void *> tls;

    QMutex mutex;

# ifdef Q_OS_SYMBIAN
    RThread symbian_thread_handle;
# endif
};

仔细看看这个结构,它几个主要的成员,eventDispatcher,就是我们刚刚说的QEventDispatcherWin32实例。eventLoops,这个是嵌套的消息循环,以及loopLevel,是它嵌套的层数(如QEventLoop::exec里面调用QEventLoop:exec)。里面还有个postEventList,表示当前的Qt事件队列,thread表示它当前所在的线程,以及一个_ref引用计数。

QThreadData奇妙在,它是跟着线程走的。在QThreadData::current中我们可以看到:

QThreadData *QThreadData::current()
{
    qt_create_tls();
    QThreadData *threadData = reinterpret_cast<QThreadData *>(TlsGetValue(qt_current_thread_data_tls_index));
    if (!threadData) {
       ...
        } else {
            threadData = new QThreadData;
            // This needs to be called prior to new AdoptedThread() to
            // avoid recursion.
            TlsSetValue(qt_current_thread_data_tls_index, threadData);
            QT_TRY {
                threadData->thread = new QAdoptedThread(threadData);
            } QT_CATCH(...) {
                TlsSetValue(qt_current_thread_data_tls_index, 0);
                threadData->deref();
                threadData = 0;
                QT_RETHROW;
            }
            threadData->deref();
        }
        ...
    }
    return threadData;
}

我们发现,调用此方法后,如果线程栈的局部存储区中没有QThreadData,一个新的QThreadData就会被创建,并且设置到当前线程的局部存储区,并且将当前线程绑定在一个假的QAdoptedThread中。

接下来是最重要的一点:所有的QObject中都有QThreadData的成员,并且有下列初始化:

QObject::QObject(QObjectPrivate &dd, QObject *parent)
    : d_ptr(&dd)
{
    Q_D(QObject);
    d_ptr->q_ptr = this;
    d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current();
    d->threadData->ref();
    .....
}

这样就非常清晰明了了,我创建一个QObject对象,它的threadData,将和parent一致。若parent没有threadData,或者是没有parent,将调用QThreadData::current获取一个新的、当前线程的QThreadData,并且将当前线程设置为一个QAdoptedThread的实例。

下面看一下QEventLoop::processEvents的实现,QEventLoop::exec()最终调入此处:

bool QEventLoop::processEvents(ProcessEventsFlags flags)
{
    Q_D(QEventLoop);
    if (!d->threadData->eventDispatcher)
        return false;
    if (flags & DeferredDeletion)
        QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete);
    return d->threadData->eventDispatcher->processEvents(flags);
}

原来QEventLoop作为一个QObject,它也有threadData。同一个线程threadData只创建一次,所以它们取出来的eventDispatcher也都是相同的。这意味着所有的相同线程的QObject,共享一份threadData,也就是同一份eventDispatcher, postEventList等。这也就说明了,我们下图是如何实现的:

事件保存在QThreadData::postEventList中,不同线程有不同的QThreadData实例

3. QThread

接下来看看当我们创建一个线程时,会发生什么:

QThreadPrivate::QThreadPrivate(QThreadData *d)
    : QObjectPrivate(), running(false), finished(false), terminated(false), exited(false), returnCode(-1),
      stackSize(0), priority(QThread::InheritPriority), data(d)
{
    ...
    if (!data)
        data = new QThreadData;
}

QThread::QThread(QObject *parent)
    : QObject(*(new QThreadPrivate), parent)
{
    Q_D(QThread);
    // fprintf(stderr, "QThreadData %p created for thread %p\n", d->data, this);
    d->data->thread = this;
}

可以看到,当新建一个QThread时,二话不说它先是建立了一个新的QThreadData,并设置thread为自己。和QThreadData::current不同的是,QThreadData::current是被动生成一个QThreadData,因为它并没有指定某个QThread。而创建QThread则可以“名正言顺”创建QThreadData,然后将它的thread设置为自己。由于它还没有执行,因此并没有设置TLS。

当一个QThread要开始执行后:

void QThread::start(Priority priority)
{
    ...
    d->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start,
                                            this, CREATE_SUSPENDED, &(d->id));
    ...
}


unsigned int __stdcall QThreadPrivate::start(void *arg)
{
    QThread *thr = reinterpret_cast<QThread *>(arg);
    QThreadData *data = QThreadData::get2(thr);

    qt_create_tls();
    TlsSetValue(qt_current_thread_data_tls_index, data);

    ...

    data->quitNow = false;
    // ### TODO: allow the user to create a custom event dispatcher
    createEventDispatcher(data);
    ...
}

可以看到,TLS被设置成了刚刚QThread创建的QThreadData实例,那么之后在这个线程中,QThreadData::current就可以取到对应的信息了。它紧接着创建了event dispatcher,也就是QEventDispatcherWin32,并且塞给了QThreadData,保证这个线程中的消息循环都是通过此QEventDispatcherWin32。

需要注意的是,如果不是通过QThread创建的QThreadData(即通过QThreadData::current来创建的)默认是没有event dispatcher的,所以你无法对一个孤立的QObject分发事件。QCoreApplication并没有继承QThread,它通过QThreadData::current获取了实例后自己设置了event dispatcher来实现消息的分发。

这样一来一切都说得通了,为什么事件是跟着线程走的,为什么每个线程都有独立的消息循环,为什么需要moveToThread,一切原因,都在QThreadData里。

4. QWidget消息循环

刚刚看到每一个QEventDispatcherWin32都会创建一个隐藏的窗口来处理WM_TIMER等事件,对于普通的QWidget来说,它们的消息处理函数叫做QtWndProc,定义在了qapplication_win.cpp中。它里面无非就是将拿到的HWND映射到正确的QWidget中,然后翻译成Qt事件。Qt很巧妙地将QWidget强行转换为了QETWidget,实现了私有成员的封装,不过这个就已经超过我们讨论的范畴了。

以上便是Qt消息循环和线程相关的秘密,虽然Qt5的代码还没有仔细研究过,但是大体上变化应该不大,希望大家看完后,能对Qt有一个【卧槽好屌啊】这样的感受。

欢迎关注我的小程序,小程序内容与网站自动保持同步

欢迎关注我的微信公众号,本网站所有的文章以及更新以后都会手动同步到微信公众号上。


公交车司机终于在众人的指责中将座位让给了老太太