文章目录
前言
1、什么是Qt消息循环
- Qt消息循环,就是从一个队列中不断取出消息,并响应消息的过程。窗体的鼠标、键盘、输入法、绘制,各种消息,都来自于Qt的消息循环。以Windows操作系统为例,Qt接管Windows原生窗口消息,并翻译成Qt的消息,派发给程序下的各个子对象、子QWidget等,通过接管层,可以很好屏蔽不同平台之间的差异性,开发人员不需要关心
Windows
或者X11
的消息的差异性,只需要搞清楚各个QEvent
之间是什么含义。 - 最开始的Qt消息循环开始于
QCoreApplication::exec
。用户创建出一个QCoreApplication
,或者说更多情况下是QApplication
,执行QCoreApplication::exec
,一个应用程序便开始了。QCoreApplication
会不断从操作系统获取消息,并且分发给QObject
。 - 如果没有消息循环,那么Qt的信号和槽无法完全使用,有些函数也无法正确执行。举个例子,通过
QueuedConnection
连接的信号,其实是将一个事件压入了消息循环,如果没有QCoreApplication::exec
那么这个消息循环将永远无法派发到指定的对象。
2、什么是线程相关性
准确点来说,应该是指QObject的线程相关性。
- 当我们创建一个
QObject
时,它会与创建自己所在的线程绑定。它参与的消息循环,其实是它所在线程的消息循环,如上图所示。假如某个线程没有默认的QThread::exec
,那么该线程上的QObject
则无法接收到事件。另外,如果两个不同线程的QObject
需要相互通信,那么只能通过QueuedConnection
的方式,异步通知对方线程,在下一轮消息循环处理QObject
的消息。 QObject
的线程相关性默认会和它的parent
保持一致。如果一个QObject
没有parent
,那么可以通过moveToThread
,将它的线程相关性切换到指定线程。- 了解
QObject
的线程相关性非常重要,很多初学者常常分不清一个多线程中哪些QObject
应该由主线程创建,哪些应该由工作线程创建,我的观点是,它参与哪个消息循环,就由哪个来创建。 - 正因为这样的特性,我们才可以理解什么叫做
AutoConnection
。通过AutoConnect
连接的两个QObject
,如果是在同一个线程,那么可以直接调用DirectConnection
,如果不是在同一个线程,那么就通过事件通知的方式QueuedConnection
来调用。通过信号和槽、事件或者QueuedConnection
方式来进行线程间的通讯,尤其是与UI线程通讯,永远是最优雅的方式之一。
一、什么是消息循环
以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有一个【卧槽好屌啊】这样的感受。