QT中的线程与事件循环理解(2)

 

1. Qt多线程与Qobject的关系安全

  每个 Qt 应用程序至少有一个事件循环,就是调用了QCoreApplication::exec()的那个事件循环。不过,QThread也能够开启事件循环。只不过这是一个受限于线程内部的事件循环。所以咱们将处于调用main()函数的那个线程,而且由QCoreApplication::exec()建立开启的那个事件循环成为主事件循环,或者直接叫主循环。注意,QCoreApplication::exec()只能在调用main()函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,由于全部有关 GUI 的操做都必须在这个线程进行。QThread局部事件循环则能够经过在QThread::run()中调用QThread::exec()开启:多线程

class Thread : public QThread
{
protected:
    void run() {
        
    }
};

  注意:Qt 4.4 版本之后,QThread::run()再也不是纯虚函数,它会调用QThread::exec()函数。与QCoreApplication同样,QThread也有QThread::quit()QThread::exit()函数来终止事件循环并发

run 函数是作什么用的?Manual中说的清楚:   app

run 对于线程的做用至关于main函数对于应用程序。它是线程的入口,run的开始和结束意味着线程的开始和结束。ide

The run() implementation is for a thread what the main() entry point is for the application. All code executed in a call stack that starts in the run() function is executed by the new thread, and the thread finishes when the function returns.函数

  线程的事件循环用于为线程中的全部QObjects对象分发事件;默认状况下,这些对象包括线程中建立的全部对象或者是在别处建立完成后被移动到该线程的对象(咱们会在后面详细介绍“移动”这个问题)。咱们说,一个QObject的所依附的线程(thread affinity)是指它所在的那个线程。它一样适用于在QThread的构造函数中构建的对象:post

class MyThread : public QThread
{
public:
    MyThread()
    {
        otherObj = new QObject;
    }    
 
private:
    QObject obj;
    QObject *otherObj;
    QScopedPointer yetAnotherObj;
};

  上面线程对象中的子成员:obj, 以及otherObj所指向的对象,以及yetAnotherObj,都在使建立Mytherad的线程,即主线程,而不是子线程。ui

  在咱们建立了MyThread对象以后,objotherObjyetAnotherObj的线程依附性是怎样的?是否是就是MyThread所表示的那个线程?要回答这个问题,咱们必须看看到底是哪一个线程建立了它们:实际上,是调用了MyThread构造函数的线程建立了它们。所以,这些对象不在MyThread所表示的线程,而是在建立了MyThread的那个线程中  this

(1)QObject::connectspa

    涉及信号槽,咱们就躲不过 connect 函数,只是这个函数你们太熟悉。我很差意思再用一堆废话来描述它,但不说又不行,那么折中一下,只看它的最后一个参数吧(为了简单起见,只看它最经常使用的3个值):

  经过指定connect的链接方式,若是指定直接链接(Direct Connection),则该槽函数将再信号发出的线程中直接执行,而不用断定当前信号发出的线程与槽函数所在线程的状态;若是指定队列链接(Queued Connection),则该槽函数在接受者所依附的线程的线程循环中被指定调用;若是为自动链接(Auto Connection)须要断定发射信号的线程和接受者所依附的线程是否相同,进行细分指定。

 

  • 自动链接(Auto Connection)
    • 这是默认设置
    • 若是发送者的信号(不是发送者对象)在接收者所依附的线程内发射,则等同于直接链接
    • 若是发射信号的线程接受者所依附的线程不一样,则等同于队列链接
    • 也就是这说,只存在下面两种状况
  • 直接链接(Direct Connection)
    • 当信号发射时,槽函数将直接被调用
    • 不管槽函数所属对象在哪一个线程,槽函数都在发射信号的线程内执行
  • 队列链接(Queued Connection)
    • 控制权回到接受者所依附线程的事件循环时,槽函数被调用
    • 槽函数在接收者所依附线程执行

(2)qT线程管理的原则:

  • QThread 是用来管理线程的,它所依附的线程它管理的线程并非同一个东西
  • QThread 所依附的线程,就是执行 QThread t 或 QThread * t=new QThread 所在的线程。也就是我们这儿的主线程
  • QThread 管理的线程,就是 run 启动的线程。也就是次线程
  • 由于QThread的对象依附在主线程中,所以他的slot函数会在主线程中执行,而不是次线程。除非:
    • QThread 对象依附到次线程中(经过movetoThread)
    • slot 和信号是直接链接(经过connect链接方式来指定),且信号在次线程中发射

【1】主线程(信号)QThread(槽)

class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(){} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
 
class Thread:public QThread 
{ 
    Q_OBJECT 
public: 
    Thread(QObject* parent=0):QThread(parent) 
    { 
        //moveToThread(this); 
    } 
public slots: 
    void slot_main() 
    { 
        qDebug()<<"from thread slot_main:" <<currentThreadId(); 
    } 
protected: 
    void run() 
    { 
        qDebug()<<"thread thread:"<<currentThreadId(); 
        exec(); 
    } 
}; 

#include "main.moc" 

int main(int argc, char *argv[]) 
{  
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    Thread thread; //槽函数所在的对象依附于线程,
    Dummy dummy; 
    QObject::connect(&dummy, SIGNAL(sig()), &thread, SLOT(slot_main())); //采用默认的连接方式
    thread.start(); 
    dummy.emitsig();//信号在主线程中发射 return a.exec(); 
}

程序运行结果:

main thread: 0x1a40 
from thread slot_main: 0x1a40
thread thread: 0x1a48

  由于connect采用默认的连接方式,则须要断定发射信号的线程和接受者所依附的线程是否相同,信号在主线程中发射 槽函数所在的对象依附于线程, 所以连接方式是直接链接,从而运行结果是:槽函数的线程Id和主线程ID是同样的!

   由于slot和run处于不一样线程,须要线程间的同步!

  你会发现 QThread 中 slot 和 run 函数共同操做的对象,都会用QMutex锁住。由于此时run 是另外一个线程,即子线程。而slot则是在主线程执行。必须适应锁来保证数据同步

  若是想让槽函数slot在次线程运行(好比它执行耗时的操做,会让主线程卡死)

  • 将 thread 依附的线程改成次线程不就好了,这也是代码中注释掉的 moveToThread(this)所作的

  去掉注释,你会发现slot在次线程中运行结果:

main thread: 0x13c0 
thread thread: 0x1de0 
from thread slot_main: 0x1de0

  但这是 Bradley T. Hughes 强烈批判的用法。不推荐这样使用

【2】run中信号与QThread中槽

  即,信号在子线程发射,而槽函数谁线程的槽函数,而线程对象在主线程中建立,若是连接采用自动连接,则条件判断比为队列链接,且由主线程在主线程的事件循环中执行。以下所示:

class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(QObject* parent=0):QObject(parent){} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
class Thread:public QThread 
{ 
    Q_OBJECT 
public: 
    Thread(QObject* parent=0):QThread(parent) 
    { 
        //moveToThread(this); 
    } 
public slots: 
    void slot_thread() 
    { 
        qDebug()<<"from thread slot_thread:" <<currentThreadId(); 
    } 
signals: 
    void sig(); 
protected: 
    void run() 
    { 
        qDebug()<<"thread thread:"<<currentThreadId(); 
        Dummy dummy; 
        connect(&dummy, SIGNAL(sig()), this, SLOT(slot_thread())); 
        dummy.emitsig(); 
        exec(); 
    } 
}; 
#include "main.moc" 
int main(int argc, char *argv[]) 
{ 
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    Thread thread; 
    thread.start(); 
    return a.exec(); 
}
View Code

运行结果:槽函数在主线程中执行。

main thread: 0x15c0

thread thread: 0x1750

from thread slot_thread: 0x15c0

  若是指定为直接链接方式,则槽函数将在次线程(信号发出的线程)执行,这样,你须要处理slot和它的对象所在线程的同步。须要 QMutex 一类的东西

推荐的方法

  其实,这个方法太简单,太好用了。定义一个普通的QObject派生类,而后将其对象move到QThread中。使用信号和槽时根本不用考虑多线程的存在。也不用使用QMutex来进行同步,Qt的事件循环会本身自动处理好这个。

 

 
class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(QObject* parent=0):QObject(parent)     {} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
 
class Object:public QObject 
{ 
    Q_OBJECT 
public: 
    Object(){} 
public slots: 
    void slot() 
    { 
        qDebug()<<"from thread slot:" <<QThread::currentThreadId(); 
    } 
}; 
 
#include "main.moc" 
 
int main(int argc, char *argv[]) 
{ 
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    QThread thread; 
    Object obj; 
    Dummy dummy; 
    obj.moveToThread(&thread); // 必须在对象的依附线程中执行此函数
    QObject::connect(&dummy, SIGNAL(sig()), &obj, SLOT(slot())); 
    thread.start(); 
    dummy.emitsig(); 
    return a.exec(); 
}

  执行结果:

main thread: 0x1a5c 
from thread slot: 0x186c

   确实简单,只须要再object的子类中新建“耗时功能”的实现“便可,而后将此对象moveToThread 到线程对象便可。

2. QT多线程原则

  咱们能够经过调用QObject::thread()能够查询一个QObject线程依附性

  注意,QCoreApplication对象以前建立的QObject没有所谓线程依附性,所以也就没有对象为其派发事件。也就是说,实际是QCoreApplication建立了表明主线程的QThread对象

  

  咱们可使用线程安全的QCoreApplication::postEvent()函数向一个对象发送事件。它将把事件加入到对象所在的线程的事件队列中,所以,若是这个线程没有运行事件循环,即没有依附的线程,这个事件也不会被派发。可是能够经过将这种浮游对象经过QObject::moveToThread()来移入到一个已有的线程中,从而确保这些浮游的对象能够依附线程。

   值得注意的一点是,虽然QObject是可重入的,可是 GUI 类,特别是QWidget及其全部的子类,都是否是可重入的。它们只能在主线程使用。因为这些 GUI 类大都须要一个事件循环,因此,调用QCoreApplication::exec()也必须是主线程,不然这些 GUI 类就没有事件循环了。你不能有两个线程同时访问一个QObject对象,除非这个对象的内部数据都已经很好地序列化(例如为每一个数据访问加锁)。记住,在你从另外的线程访问一个对象时,它可能正在处理所在线程的事件循环派发的事件!基于一样的缘由,你也不能在另外的线程直接delete一个QObject对象,相反,你须要调用QObject::deleteLater()函数,这个函数会给对象所在线程发送一个删除的事件。

(1)QObject的线程依附性是能够改变的,方法是调用QObject::moveToThread()函数。该函数会改变一个对象及其全部子对象的线程依附性。因为QObject不是线程安全的,因此咱们只能在该对象所在线程上调用这个函数。也就是说,咱们只能在对象所在线程将这个对象移动到另外的线程,不能在另外的线程改变对象的线程依附性。

(2)Qt 要求QObject的全部子对象都必须和其父对象在同一线程。这意味着:

  • 不能对有父对象(parent 属性)的对象使用QObject::moveToThread()函数
  • 不能在QThread中以这个QThread自己做为父对象建立对象,,这是由于要建立该线程对象必然在其余的线程中建立,即该线程对象必然依附于其余线程对象,而以该线程对象为父类的子对向,在run函数中进行新建子类对象,若以其做为父对象,则与QT所定义的原则冲突,所以禁止。
class Thread : public QThread {
    void run() {
        QObject *obj = new QObject(this); // 错误!
    }
};

  这是由于QThread对象所依附的线程是建立它的那个线程,而不是它所表明的线程。

 (3)Qt 还要求,在表明一个线程的QThread对象销毁以前,全部在这个线程中的对象都必须先delete

  要达到这一点并不困难:咱们只需在QThread::run()栈空间(直接定义对象)上建立对象便可。

 

   如今的问题是,既然线程建立的对象都只能在函数栈上,怎么能让这些对象与其它线程的对象通讯呢?Qt 提供了一个优雅清晰的解决方案:咱们在线程的事件队列中加入一个事件,而后在事件处理函数中调用咱们所关心的函数。显然这须要线程有一个事件循环。这种机制依赖于 moc 提供的反射:所以,只有信号、槽和使用Q_INVOKABLE宏标记的函数能够在另外的线程中调用。

   QMetaObject::invokeMethod()静态函数会这样调用:

QMetaObject::invokeMethod(object, "methodName",
                          Qt::QueuedConnection,
                          Q_ARG(type1, arg1),
                          Q_ARG(type2, arg2));

  上面函数调用中出现的参数类型都必须提供一个公有构造函数,一个公有的析构函数和一个公有的复制构造函数,而且要使用qRegisterMetaType()函数向 Qt 类型系统注册。

  跨线程的信号槽也是相似的。当咱们将信号与槽链接起来时,QObject::connect()的最后一个参数将指定链接类型:

  • Qt::DirectConnection直接链接意味着槽函数将在信号发出的线程直接调用
  • Qt::QueuedConnection队列链接意味着向接受者所在线程发送一个事件,该线程的事件循环将得到这个事件,而后以后的某个时刻调用槽函数
  • Qt::BlockingQueuedConnection阻塞的队列链接就像队列链接,可是发送者线程将会阻塞,直到接受者所在线程的事件循环得到这个事件,槽函数被调用以后,函数才会返回
  • Qt::AutoConnection自动链接(默认)意味着若是接受者所在线程就是当前线程,则使用直接链接;不然将使用队列链接;即若是接受者依附的线程就是当前线程,则直接链接,信号发出就调用。若是接受者依附的线程是其余线程,则队列链接,即向接受者所在线程发送一个事件,该线程的事件循环将得到这个事件,而后以后的某个时刻调用槽函数。

  注意在上面每种状况中,发送者所在线程都是可有可无的!在自动链接状况下,Qt 须要查看信号发出的线程是否是与接受者所在线程一致,来决定链接类型。注意,Qt 检查的是信号发出的线程,而不是信号发出的对象所在的线程!咱们能够看看下面的代码:

class Thread : public QThread
{
Q_OBJECT
signals:
    void aSignal();
protected:
    void run() {
        emit aSignal();
    }
};
 
/* ... */
Thread thread;
Object obj;
QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
thread.start();

  aSignal()信号在一个新的线程被发出(也就是Thread所表明的线程)。注意,由于这个线程并非Object所在的线程(Object所在的线程和Thread所在的是同一个线程),可是aSignal()确实在Thread所表明的新线程中发出,所以,必然是队列链接

3. 多线程的数据交互,数据同步问题

  线程对象依附的线程VS线程表明的新线程执行中对原有线程的访问问题

class Thread : public QThread
{
Q_OBJECT
slots:
    void aSlot() {
        /* ... */
    }
protected:
    void run() {
        /* ... */
    }
};
 
/* ... */
Thread thread;
Object obj;
QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
thread.start();
obj.emitSignal();

  这里的obj发出aSignal()信号时,使用哪一种链接方式?答案是:直接链接。由于Thread对象所在线程发出了信号,也就是信号发出的线程与接受者是同一个。在aSlot()槽函数中,咱们能够直接访问Thread的某些成员变量,可是注意,在咱们访问这些成员变量时,Thread::run()函数可能也在访问!这意味着两者并发进行这是一个完美的致使崩溃的隐藏bug

    

class Thread : public QThread
{
Q_OBJECT
slots:
    void aSlot() {
        /* ... */
    }
protected:
    void run() {
        QObject *obj = new Object;
        connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));
        /* ... */
    }
};

  上面也是队列链接,且Thread的aSlot槽函数依附于Thread对象的依附线程,即主线程。所以与子线程中的信号不在同一个线程中。

  若是为了在子线程中调用线程对象自己的槽函数,且槽函数的执行也在该子线程中。则采用

1. 在线程的构造函数中使用QObject::moveToThread()方法

2. 直接指定链接方式为 :Driect链接方式

3. 采用上文中推荐的方法。

 第一种:在线程构造函数中,QThread对象不是线程自己,将改对象依附到其本身所建立的线程中。

  实际上,这的确可行(由于Thread的线程依附性被改变了:它所在的线程成了本身),可是这并非一个好主意。这种代码意味着咱们其实误解了线程对象(QThread子类)的设计意图:它们实际上是用于管理它所表明的线程的对象。所以,它们应该在另外的线程被使用(一般就是它本身所在的线程),而不是在本身所表明的线程中。

class Thread : public QThread {
Q_OBJECT
public:
    Thread() {
        moveToThread(this); // 错误!,不推荐
    }
 
    /* ... */
};

第二种:也不推荐

第三种:最好的解决方式,就是采用上面提到的,咱们能够利用一个QObject的子类,使用QObject::moveToThread()改变其线程依附性:将处理任务的部分与管理线程的部分分离。

 

 

 

 

 

endl;

相关文章
相关标签/搜索