【QT】深入了解QT消息循环及线程相关性
一、什么是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有一个【卧槽好屌啊】这样的感受。
参考链接: