首页 Qt 学习之路 2 Qt 学习之路 2(74):线程和 QObject

Qt 学习之路 2(74):线程和 QObject

22 3.7K

前面两个章节我们从事件循环和线程类库两个角度阐述有关线程的问题。本章我们将深入线程间得交互,探讨线程和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() {
        /* ... 初始化 ... */
        exec();
    }
};

记得我们前面介绍过,Qt 4.4 版本以后,QThread::run()不再是纯虚函数,它会调用QThread::exec()函数。与QCoreApplication一样,QThread也有QThread::quit()QThread::exit()函数来终止事件循环。

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

class MyThread : public QThread
{
public:
    MyThread()
    {
        otherObj = new QObject;
    }    

private:
    QObject obj;
    QObject *otherObj;
    QScopedPointer yetAnotherObj;
};

在我们创建了MyThread对象之后,objotherObjyetAnotherObj的线程依附性是怎样的?是不是就是MyThread所表示的那个线程?要回答这个问题,我们必须看看究竟是哪个线程创建了它们:实际上,是调用了MyThread构造函数的线程创建了它们。因此,这些对象不在MyThread所表示的线程,而是在创建了MyThread的那个线程中。

我们可以通过调用QObject::thread()可以查询一个QObject的线程依附性。注意,在QCoreApplication对象之前创建的QObject没有所谓线程依附性,因此也就没有对象为其派发事件。也就是说,实际是QCoreApplication创建了代表主线程的QThread对象。

线程和QObject

我们可以使用线程安全的QCoreApplication::postEvent()函数向一个对象发送事件。它将把事件加入到对象所在的线程的事件队列中,因此,如果这个线程没有运行事件循环,这个事件也不会被派发。

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

QObject的线程依附性是可以改变的,方法是调用QObject::moveToThread()函数。该函数会改变一个对象及其所有子对象的线程依附性。由于QObject不是线程安全的,所以我们只能在该对象所在线程上调用这个函数。也就是说,我们只能在对象所在线程将这个对象移动到另外的线程,不能在另外的线程改变对象的线程依附性。还有一点是,Qt 要求QObject的所有子对象都必须和其父对象在同一线程。这意味着:

  • 不能对有父对象(parent 属性)的对象使用QObject::moveToThread()函数
  • 不能在QThread中以这个QThread本身作为父对象创建对象,例如下面的代码片段:这是因为QThread对象所依附的线程是创建它的那个线程,而不是它所代表的线程。
class Thread : public QThread {
    void run() {
        QObject *obj = new QObject(this); // 错误!
    }
};

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所在的是同一个线程,回忆下,信号槽的连接方式与发送者所在线程无关),所以这里将会使用队列连接。

另外一个常见的错误是:

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()));
        /* ... */
    }
};

这个例子也会使用队列连接。然而,这个例子比上面的例子更具隐蔽性:在这个例子中,你可能会觉得,Object所在Thread所代表的线程中被创建,又是访问的Thread自己的成员数据。稍有不慎便会写出这种代码。

为了解决这个问题,我们可以这么做:Thread构造函数中增加一个函数调用:moveToThread(this)

class Thread : public QThread {
Q_OBJECT
public:
    Thread() {
        moveToThread(this); // 错误!
    }

    /* ... */
};

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

上面问题的最好的解决方案是,将处理任务的部分与管理线程的部分分离。简单来说,我们可以利用一个QObject的子类,使用QObject::moveToThread()改变其线程依附性:

class Worker : public QObject
{
Q_OBJECT
public slots:
    void doWork() {
        /* ... */
    }
};

/* ... */
QThread *thread = new QThread;
Worker *worker = new Worker;
connect(obj, SIGNAL(workReady()), worker, SLOT(doWork()));
worker->moveToThread(thread);
thread->start();

22 评论

番茄 2013年12月25日 - 08:58

豆子,请教您一个问题?
在Qt线程函数run()中使用QTimer有什么比较好的方法吗?我是想定义一个类,在类中使用定时器,但是这个类用在run()中,就不可以使用。程序报错。

万分感谢!!

回复
豆子 2013年12月26日 - 21:23

方便发送一下代码到邮箱或者QQ吗?线程的问题还是根据代码比较好解决的。

回复
ngz 2014年7月29日 - 08:24

其实俺真正感兴趣的是c++ 11 多线程和 qt 信号槽怎么连用

回复
豆子 2014年7月29日 - 09:26

这一点不是很了解,也很少有资料说明。只是在使用 Qt 信号槽的时候一般很少涉及 std 的东西。如果你需要使用 C++ 11 的线程,不妨试试 boost 的信号槽。

回复
华冬 2014年8月9日 - 08:05

请问楼主,如何控制线程的暂停,恢复,停止,重新开始呢,求指教

回复
豆子 2014年8月10日 - 13:51

QThread提供了wait()函数,用于等待一段时间。不过暂停之类的貌似没有。这样的话应该在run()函数中使用变量对执行代码进行控制,以便达到暂停的效果。

回复
佚之狐L 2014年10月9日 - 14:54

注意,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所在的是同一个线程,回忆下,信号槽的连接方式与发送者所在线程无关),所以这里将会使用队列连接。

另外一个常见的错误是:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
下面说另一个错误,也就是代码也是一个错误,“所以这里将会使用队列连接”是不是应该为“所以这里将会使队列连接”? 消息队列连接是指出现了死循环?运行了例子,系统报SIGSEGV(这个不是空指针吗?)
下面问题来了:
自己做了个简单的com收发模块
接收线程的指针作为一个成员变量在类里面:
m_pRecvThread = new QRecvThread(this);
m_pRecvThread->start();

connect(m_pRecvThread, SIGNAL(COM_RECVDATA(QByteArray)), this, SLOT(slotRecvData(QByteArray)));
我这个跑的挺好的啊?没看出和上面的错误代码有什么区别,只是我在run()里有个大while循环,线程一直没退。没理解Object所在的线程概念,简单说就是没明白上面例子代码错在哪里了

回复
佚之狐L 2014年10月9日 - 22:33

另外,能不能改下不用google的font,或者异步加载一下

回复
独角兽6712 2015年1月4日 - 17:17

楼主 我现在用qt写了一个线程循环接收串口数据并把每次收到的数据都append到缓存到变量Qbytearray temp,但是我在其它地方还会重复thread.start(),多次启动线程,只要第二次启动thread时temp的数据就会发生错误,(比如 我第一次启动线程,发送数据a ,qdebug出temp 是a,这时我再调用thread.start(),然后发送数据b,这时temp应该打印出ab,但它却只打印出b,然后再发数据c,应该打印abc,它又只打印出ac,)我认为应该是重复启动线程的问题,但不知道具体错误出在哪,也不知道怎样在下次启动这一线程时先将线程彻底销毁,求楼主指教

回复
qt学习 2015年1月8日 - 15:37

豆子,我觉得这个系列讲解qt讲的不错,就是线程和事件循环以及内存处理这块没讲好,尤其是线程这块,虽然讲了几章,但是我仔细看却看得一头雾水

回复
豆子 2015年1月9日 - 18:26

不好意思,这部分内容过于抽象,而且例子也不是很好设计,所以可能不好理解

回复
xzp21st 2015年1月28日 - 15:50

发错了 我想问一下:用了movetothread将对象依附于一个线程后,那对象的子对象是不是也依附到这个线程了。我现在的问题是对象有一个子对象QTimer,然后在对象的成员函数(afun)中start(这个成员函数我设置为槽函数),然后将这个对象movetothread。然后在主线程中用connect连接一个启动信号到之前那个对象的成员函数(afun),运行后会提示QObject::startTimer: timers cannot be started from another thread 不知道什么原因?

回复
xzp21st 2015年1月29日 - 13:40

这个弄明白了 movetothread并没有把对象及其子对象move到子线程,而是将和主线程connect过的slot函数move到了子线程运行

回复
王先先 2015年10月10日 - 09:06

Object obj;
QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));

=========================================================
豆子大神,,一直没看懂为什么obj对象 会发出aSignal()信号、、、

回复
豆子 2015年10月13日 - 13:07

这只是一个演示性质的语句,并不是真实存在的,仅仅用来说明语法

回复
zhangwen 2016年2月26日 - 15:20

不能对有父对象(parent 属性)的对象使用QObject::moveToThread()函数。我看worker对象有父对象,为什么可以使用?
#include class Worker : public QObject { Q_OBJECT private slots: void onTimeout() { qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId(); } }; #include "main.moc" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug()<<"From main thread: "<<QThread::currentThreadId(); QThread t; QTimer timer; Worker worker; QObject::connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout())); timer.start(1000); worker.moveToThread(&t); t.start(); return a.exec(); }

回复
小怪 2016年5月24日 - 15:53

博主您好 向你讨教
本人做一个应用程序 想通过设计一个参数对话框将参数传递给在次线程运行的算法函数。起初想到的就是发送一个带参信号给线程槽函数来实现。看到博主这里分析说不要再Qthread子类使用槽函数。那还有什么较好的办法实现主线程向次线程传递参数。

回复
LongHua 2016年9月8日 - 17:49

博主,”Qt 检查的是信号发出的线程,而不是信号发出的对象所在的线程“ 这句话应该有误,理由如下:
// 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;

回复
豆子 2016年9月10日 - 22:42

这段代码出自 qobject.cpp (Src/qtbase/src/corelib/kernel/qobject.cpp),Qt 5.7 的代码片段如下:

Qt::HANDLE currentThreadId = QThread::currentThreadId();
...
QObject * const receiver = c->receiver;
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_index, c, argv ? argv : empty_argv, locker);
    continue;
...

receiverInSameThread 变量是根据 currentThreadId 和 receiver 的 threadId 来判断的。因此在讨论这个问题时,首先要考虑,这个 currentThreadId 究竟是哪一个线程的 threadId;按照 currentThreadId 的定义,取的是 QThread::currentThreadId(),也就是执行函数的当前线程的 threadId。那么问题就是,这个函数是在哪里调用的。这段代码来自QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv),按照这里(原文翻译在这里)的说法,这个函数是由 moc 生成的 signal 中调用的,因此实际是来自emit signal();这样的语句;也就是发出信号的线程,这与发出该信号的对象所在线程无关。所以我认为原文中的说法是正确的。

回复
William 2016年10月28日 - 14:58

写得很棒,言简意赅,让我对QT线程有了初步的认识,谢谢豆子。

回复
LinWM 2017年7月25日 - 10:58

“QObject及其所有子类都不是线程安全的(但都是可重入的)”错误指出:Although QObject is reentrant, the GUI classes, notably QWidget and all its subclasses, are not reentrant. They can only be used from the main thread. As noted earlier, QCoreApplication::exec() must also be called from that thread.
摘自:http://doc.qt.io/qt-5/threads-qobject.html

回复
LinWM 2017年7月25日 - 19:14

看完这一篇,发现多有错误,措辞存在歧义性,表达也比较凌乱:建议小伙伴对照英文文档理解,如果实在不好理解建议跳过“线程”这些章节;

回复

回复 豆子 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2