qt messagebox退出程序_深入了解QT消息循环及线程相关性

59c1a64aac761027374c691575ec78fa.png

前言

本篇文章作者为 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文档中的示意图来作说明:

babad6751c95b49a8f20208320825799.png

当我们创建一个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用它创建了一个不可见的窗口,并且为它注册了一个叫做qtinternalproc的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都会创建一个隐藏的窗口来处理WMTIMER等事件,对于普通的QWidget来说,它们的消息处理函数叫做QtWndProc,定义在了qapplicationwin.cpp中。它里面无非就是将拿到的HWND映射到正确的QWidget中,然后翻译成Qt事件。Qt很巧妙地将QWidget强行转换为了QETWidget,实现了私有成员的封装,不过这个就已经超过我们讨论的范畴了。

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

14efb37a08bdcaefedf1878af71e1fec.png

894b7e25727257e40cd7d7fc6e299132.gif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值