QObject 之 Thread Affinity

http://blog.csdn.net/dbzhang800/article/details/6557272

QObject Reentrancy

  • The child of a QObject must always be created in the thread where the parent was created. This implies, among other things, that you should never pass the QThread object (this) as the parent of an object created in the thread (since the QThreadobject itself was created in another thread).
  • Event driven objects may only be used in a single thread. Specifically, this applies to the timer mechanism and the network module. For example, you cannot start a timer or connect a socket in a thread that is not the object's thread.
  • You must ensure that all objects created in a thread are deleted before you delete the QThread. This can be done easily by creating the objects on the stack in your run() implementation.

注意,本文试图通过源码解释下面的问题:

  • 子QObject必须在其parent关联的线程内创建
  • 调用moveToThread()的对象其parent必须为0
  • 事件驱动的对象要在单一线程内使用
    • QTimer、network模块的QTcpSocket等等
    • 为什么不能在非关联线程内开启QTimer或者连接QTcpSocket到服务器?
  • 删除QThread对象前,确保线程内所有对象都没销毁
  • AutoConnection的是是非非,两种说法孰是孰非?

    • 其一:信号关联的线程和槽关联的线程不一致时,则Queued方式
    • 其二:信号发射时的当前线程和槽函数关联的线程不一致时,则Queued方式

但很显然,我没做到这一点(能力所限,现阶段我只能让自己勉强明白),尽管如此,本文应该还是提供了很多你理解这些问题所需的背景知识。

QObject的线程关联性

线程关联性(Thread Affinity)???

什么东西?

每一个QObject都会和一个线程相关联

QObject 是线程感知的,每一个QObject及派生类的对象被创建时都会将其所在线程的引用保存下来(可以通过QObject::thread()返回)。

干嘛用的?

用于事件系统

QObject对象的事件处理函数始终要在其所关联线程的上下文中执行。

可否改变?

 

使用QObject::moveToThread()可以将QObject对象从一个线程移动到另一个线程。

QObject

看看QObject的初始化(看两点):

  • 保存当前线程(QThreadData)的指针。
  • 如果其parent关联的线程和当前线程不一致,parent会强制置0。
    • 这要求子对象必须在其parent关联的线程内创建。
    • 当使用QThread时,你不能将QThread对象作为在新线程中所创建的对象的parent。
QObject::QObject(QObject *parent)
    : d_ptr(new QObjectPrivate)
{
    Q_D(QObject);
    d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current();
    if (parent) {
        if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData))
            parent = 0;
        setParent(parent);
}

看看moveToThread()的代码(我们此处只关心限制条件):

  • parent非0的对象不能被移动!
  • QWidget及其派生类对象不能被移动!
  • 该函数必须在对象关联的线程内调用!
void QObject::moveToThread(QThread *targetThread)
{
    Q_D(QObject);
    if (d->parent != 0) {
        qWarning("QObject::moveToThread: Cannot move objects with a parent");
        return;
    }
    if (d->isWidget) {
        qWarning("QObject::moveToThread: Widgets cannot be moved to a new thread");
        return;
    }

    QThreadData *currentData = QThreadData::current();
    QThreadData *targetData = targetThread ? QThreadData::get2(targetThread) : new QThreadData(0);
    if (d->threadData->thread == 0 && currentData == targetData) {
        // one exception to the rule: we allow moving objects with no thread affinity to the current thread
        currentData = d->threadData;
    } else if (d->threadData != currentData) {
        qWarning("QObject::moveToThread: Current thread (%p) is not the object's thread (%p)./n"
                 "Cannot move to target thread (%p)/n",
                 currentData->thread, d->threadData->thread, targetData->thread);
        return;
    }
......

moveToThread()的其他工作:

  • 生成并通过sendEvent()派发 QEvent::ThreadChange 事件

  • 解除在当前线程中的timer注册(在目标线程中重新注册)
  • 将该对象在当前事件队列中的事件移动到目标线程的事件队列中
  • ...

事件循环

QCoreApplication::exec()

我们在QDialog 模态对话框与事件循环 一文中提到:

  • 调用的是QEventLoop 的 exec()
int QCoreApplication::exec()
{
...
    QEventLoop eventLoop;
    int returnCode = eventLoop.exec();
...
    return returnCode;
}
  • exec()进而调用 QEventLoop::processEvents()
int QEventLoop::exec(ProcessEventsFlags flags)
{
    Q_D(QEventLoop);
...
    while (!d->exit)
        processEvents(flags | WaitForMoreEvents | EventLoopExec);
...
    return d->returnCode;
}
  • 进而调用本线程内的 eventDispatcher
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);
}
  • 前面注意这段代码,如果没有eventDispatcher,这个函数什么都不做。这个东西是什么时候创建的呢?
QEventLoop::QEventLoop(QObject *parent)
    : QObject(*new QEventLoopPrivate, parent)
{
    Q_D(QEventLoop);
    if (!d->threadData->eventDispatcher) {
        QThreadPrivate::createEventDispatcher(d->threadData);
    } 
}
  • 一个线程内可以创建并启动多个QEventLoop(事件循环可以嵌套,你经常这样用,只不过可能没意识到,可考虑QEventLoop使用两例 ),而第一个负责创建eventDispatcher.

QCoreApplication::postEvent()

QCoreApplicationn::postEvent()和线程有什么瓜葛?

  • 获取接收者关联的线程信息
  • 将事件放入线程的事件队列
  • 唤醒eventDispatcher
void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)
{
    QThreadData * volatile * pdata = &receiver->d_func()->threadData;
    if (data->postEventList.isEmpty() || data->postEventList.last().priority >= priority) {
        data->postEventList.append(QPostEvent(receiver, event, priority));
    } else {
        QPostEventList::iterator begin = data->postEventList.begin()
                                         + data->postEventList.insertionOffset,
                                   end = data->postEventList.end();
        QPostEventList::iterator at = qUpperBound(begin, end, priority);
        data->postEventList.insert(at, QPostEvent(receiver, event, priority));
    }
    if (data->eventDispatcher)
        data->eventDispatcher->wakeUp();
...

事件派发

无论如何,事件最终都要通过 sendEvent 和 sendSpontaneousEvent 才能派发到接收的对象中

  • send(Spontaneous)Event 直接调用notifyInternal,进而直接调用notify,最终直接调用QObject::event()

  • QObject::event()进而直接调用timerEvent()等事件处理函数
inline bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
{  if (event) event->spont = false; return self ? self->notifyInternal(receiver, event) : false; }

inline bool QCoreApplication::sendSpontaneousEvent(QObject *receiver, QEvent *event)
{ if (event) event->spont = true; return self ? self->notifyInternal(receiver, event) : false; }

因为事件由其关联的线程内的eventDispatcher进行派发,所以所有的事件处理函数都会在关联的线程内被调用。如果关联线程的事件循环没有启用呢?就不会有eventispatcher了,timerEvent等事件也就更无从谈起了。

QTimer疑问?

为何只能在其关联的线程内启动timer?

QTimer源码分析(以Windows下实现为例) 一文中,我们谈到:

QTimer的是通过QObject的timerEvent()实现的,开启和关闭定时器是通过QObject的startTimer()和killTimer完成的。

startTimer最终调用对象关联线程的eventDispatcher来注册定时器:

int QObject::startTimer(int interval)
{
    Q_D(QObject);
    return d->threadData->eventDispatcher->registerTimer(interval, this);

在Win32平台下:

void QEventDispatcherWin32::registerTimer(int timerId, int interval, QObject *object)
{
    if (timerId < 1 || interval < 0 || !object) {
        qWarning("QEventDispatcherWin32::registerTimer: invalid arguments");
        return;
    } else if (object->thread() != thread() || thread() != QThread::currentThread()) {
        qWarning("QObject::startTimer: timers cannot be started from another thread");
        return;
    }
...

在Linux平台下:

void QEventDispatcherGlib::registerTimer(int timerId, int interval, QObject *object)
{
#ifndef QT_NO_DEBUG
    if (timerId < 1 || interval < 0 || !object) {
        qWarning("QEventDispatcherGlib::registerTimer: invalid arguments");
        return;
    } else if (object->thread() != thread() || thread() != QThread::currentThread()) {
        qWarning("QObject::startTimer: timers cannot be started from another thread");
        return;
    }
...

在这两个平台下,它都会检查当前线程和dispatcher的线程是否一致。不一致则直接返回。

为什么要这么设计。我不太清楚。或许是因为:注册定时器要用到回调函数,而回调函数需要在注册的线程执行(fix me)。

Qt::AutoConnection

  • 使用connect连接信号槽时,默认是 AutoConnection

  • 使用invokeMethod时,可以指定 AutoConnection

设置AutoConnection就是让Qt帮助我们选择直连还是队列连接的方式。选择的依据就是当前的线程和接收者的关联的线程是否一致,而与信号所在对象关联的线程无关 (对Qt4.8及后续版本,这句话是对的)。

invokeMethod

这个不涉及信号的问题,处理起来很简单:比较当前线程和接收者所关联的线程是否一致即可。

  • 检查Connection的类型,处理AutoConnection

// check connection type 
    QThread *currentThread = QThread::currentThread(); 
    QThread *objectThread = object->thread(); 
    if (connectionType == Qt::AutoConnection) { 
        connectionType = currentThread == objectThread 
                         ? Qt::DirectConnection 
                         : Qt::QueuedConnection; 
    }
  • 对于 直连的,直接调 metacall,它进而去调用对象的 qt_metacall
if (connectionType == Qt::DirectConnection) { 
        return QMetaObject::metacall(object, QMetaObject::InvokeMetaMethod, methodIndex, param) < 0;
  • 对于 Queued 的连接,post 相应的事件,进而转到对象的event()函数中
if (connectionType == Qt::QueuedConnection) { 
            QCoreApplication::postEvent(object, new QMetaCallEvent(methodIndex, 
                                                                   0, 
                                                                   -1, 
                                                                   nargs, 
                                                                   types, 
                                                                   args));

connect

connect中指定了AutoConnection,信号发射时,相应槽是Direct还是Queued方式调用呢???

你应该见过两种说法:

  • 其一:信号关联的线程和槽关联的线程不一致时,则Queued方式
  • 其二:信号发射时的当前线程和槽函数关联的线程不一致时,则Queued方式

注意:在Qt4.7.3(包括)以前,前一种说法是对的(充分条件)。从Qt4.8开始,后面的说法是对的(充要条件)。

看看Qt4.7.3中QMetaObject::activate()的代码:

            // determine if this connection should be sent immediately or
            // put into the event queue
            if ((c->connectionType == Qt::AutoConnection
                 && (currentThreadData != sender->d_func()->threadData
                     || receiver->d_func()->threadData != sender->d_func()->threadData))
                || (c->connectionType == Qt::QueuedConnection)) {
                queued_activate(sender, signal_absolute_index, c, argv ? argv : empty_argv);
                continue;
            }

对比看看Qt4.8中的代码:

            const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId;

            // determine if this connection should be sent immediately or
            // put into the event queue
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                queued_activate(sender, signal_absolute_index, c, argv ? argv : empty_argv);
                continue;
            }

参考

  • Qt 源码

4.7文档:

Qt::AutoConnection    0    (default) Same as DirectConnection, if the emitter and receiver are in the same thread. Same as QueuedConnection, if the emitter and receiver are in different threads.

4.8文档:

Qt::AutoConnection 0 (default) If the signal is emitted from a different thread than the receiving object, the signal is queued, behaving as Qt::QueuedConnection. Otherwise, the slot is invoked directly, behaving as Qt::DirectConnection. The type of connection is determined when the signal is emitted

http://comments.gmane.org/gmane.comp.lib.qt.general/44460

The fix is in Qt 4.8

原文地址:https://www.cnblogs.com/cute/p/2561375.html