QObject 之 Thread Affinity

目錄(?)[+]服務器

注意,本文試圖經過源碼解釋下面的問題:app

  • 子QObject必須在其parent關聯的線程內建立函數

  • 調用moveToThread()的對象其parent必須爲0oop

  • 事件驅動的對象要在單一線程內使用源碼分析

    • QTimer、network模塊的QTcpSocket等等post

    • 爲何不能在非關聯線程內開啓QTimer或者鏈接QTcpSocket到服務器?this

  • 刪除QThread對象前,確保線程內全部對象都沒銷燬spa

  • AutoConnection的是是非非,兩種說法孰是孰非?.net

    • 其一:信號關聯的線程和槽關聯的線程不一致時,則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 源碼

相關文章
相關標籤/搜索