#include "widget.h"
#include "ui_widget.h"
#include
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
edit = new QTextEdit(this);
edit->resize(200,300);
thread=new ReadThread(this);
thread->start();
//connect(thread,&ReadThread::toLine,this,&Widget::append);
connect(thread,&ReadThread::finished,this,&Widget::FinishThread);
edit->append("开始准备");
connect(testInstance::getInstance(),&testInstance::data_process,this,[](){
static int count=0;
count+=1;
qDebug()<<"响应成功"<thread->currentThreadId();
qDebug()<<"from thread append:" <append(lineTemp);
}
void Widget::FinishThread()
{
thread->quit();
delete testInstance::getInstance();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YTAaF2WP-1616343294966)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1616335701664.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8wtpiFpu-1616343294969)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1616336131182.png)]
描述
以上程序中目前共有两个线程
一个为gui线程id 0x4954
主要负责图形界面和槽函数的处理
一个run函数id 0x53f8,
主要负责读文件和发射信号
工作原理:一股气将信号打进执行队列(以上结果部分删减),然后主线程取信号和信号参数循环解决,如果不理解,始终记住信号槽是线程安全的。
run 函数是做什么用的?Manual中说的清楚:
原文如下(这段话我们称为定理一吧):
这么短的文字一眼就看完了,可是,这是什么意思呢?又能说明什么问题呢?看段简单代码:
class Thread:public QThread
{
Q_OBJECT
public:
Thread(QObject* parent=0):QThread(parent){}
public slots:
void slot() { ... }
signals:
void sig();
protected:
void run() { ...}
};
int main(int argc, char** argv)
{
...
Thread thread;
...
}
对照前面的定理,run函数中的代码时确定无疑要在次线程中运行的,那么其他的呢?比如 slot 是在次线程还是主线程中运行?
你想说主线程,但又心有不甘,对么?
涉及信号槽,我们就躲不过 connect 函数,只是这个函数大家太熟悉。我不好意思再用一堆废话来描述它,但不说又不行,那么折中一下,只看它的最后一个参数吧(为了简单起见,只看它最常用的3个值)
下面的列表,我们暂称为定理二:
同前面一样,这些文字大家都能看懂。但含义呢?
不妨继续拿前面的例子来看,slot 函数是在主线程还是次线程中执行呢?
定理二强调两个概念:发送信号的线程 和 接收者所依附的线程。而 slot 函数属于我们在main中创建的对象 thread,即thread依附于主线程
太绕了?不是么(要彻底理解这几句话,你可能需要看Qt meta-object系统和Qt event系统)
如果上两节看不懂,就记住下面的话吧(自己总结的,用词上估计会不太准确)。
这是 Qt Manual 和 例子中普遍采用的方法。 但由于manual没说槽函数是在主线程执行的,所以不少人都认为它应该是在次线程执行了。
/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/
#include
#include
#include
#include
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();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9sOfCvLJ-1616343294971)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1616331596276.png)]
自己解释
主线程id
线程的槽函数线程id与依附线程id相同
次线程id
然后看到结果(具体值每次都变,但结论不变)
main thread: 0x1a40 from thread slot_main: 0x1a40 thread thread: 0x1a48
看到了吧,槽函数的线程和主线程是一样的!
如果你看过Qt自带的例子,你会发现 QThread 中 slot 和 run 函数共同操作的对象,都会用QMutex锁住。为什么?
因为slot和run处于不同线程,需要线程间的同步!
如果想让槽函数slot在次线程运行(比如它执行耗时的操作,会让主线程死掉),怎么解决呢?
main thread: 0x13c0
thread thread: 0x1de0
from thread slot_main: 0x1de0
这可以工作,但这是 Bradley T. Hughes 强烈批判的用法。推荐的方法后面会给出。
#ifndef DUMMY2_H
#define DUMMY2_H
/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/
#include
#include
#include
#include
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();
}
};
#endif // DUMMY2_H
想看结果么?
main thread: 0x15c0
thread thread: 0x1750
from thread slot_thread: 0x15c0
如何解决呢?
千呼万唤始出来。
其实,这个方法太简单,太好用了。定义一个普通的QObject派生类,然后将其对象move到QThread中。使用信号和槽时根本不用考虑多线程的存在。也不用使用QMutex来进行同步,Qt的事件循环会自己自动处理好这个。
#ifndef DUMMY3_H
#define DUMMY3_H
/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/
#include
#include
#include
#include
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();
}
};
#endif // DUMMY3_H
#include "widget.h"
#include
#include "dummy3.h"
int main(int argc, char *argv[])
{
QApplication a(argc, 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();
return a.exec();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BPj5hqBr-1616343294974)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1616332899415.png)]
Qt 线程基础(QThread、QtConcurrent等)
基本上有种使用线程的场合:
开发人员使用线程时需要非常小心。启动线程是很容易的,但确保所有共享数据保持一致很难。遇到问题往往很难解决,这是由于在一段时间内它可能只出现一次或只在特定的硬件配置下出现。在创建线程来解决某些问题之前,应该考虑一些替代的技术 :
替代技术 | 注解 |
---|---|
QEventLoop::processEvents() |
在一个耗时的计算操作中反复调用QEventLoop::processEvents() 可以防止界面的假死。尽管如此,这个方案可伸缩性并不太好,因为该函数可能会被调用地过于频繁或者不够频繁。 |
QTimer |
后台处理操作有时可以方便地使用Timer安排在一个在未来的某一时刻执行的槽中来完成。在没有其他事件需要处理时,时间隔为0的定时器超时事件被相应 |
QSocketNotifier QNetworkAccessManager QIODevice::readyRead() |
这是一个替代技术,替代有一个或多个线程在慢速网络执行阻塞读的情况。只要响应部分的计算可以快速执行,这种设计比在线程中实现的同步等待更好。与线程相比这种设计更不容易出错且更节能(energy efficient)。在许多情况下也有性能优势。 |
一般情况下,建议只使用安全和经过测试的方案而避免引入特设线程的概念。QtConcurrent 提供了一个将任务分发到处理器所有的核的易用接口。线程代码完全被隐藏在 QtConcurrent 框架下,所以你不必考虑细节。尽管如此,QtConcurrent 不能用于线程运行时需要通信的情况,而且它也不应该被用来处理阻塞操作。
有时候,你需要的不仅仅是在另一线程的上下文中运行一个函数。您可能需要有一个生存在另一个线程中的对象来为GUI线程提供服务。也许你想在另一个始终运行的线程中来轮询硬件端口并在有关注的事情发生时发送信号到GUI线程。Qt为开发多线程应用程序提供了多种不同的解决方案。解决方案的选择依赖于新线程的目的以及线程的生命周期。
生命周期 | 开发任务 | 解决方案 |
---|---|---|
一次调用 | 在另一个线程中运行一个函数,函数完成时退出线程 | 编写函数,使用QtConcurrent::run 运行它 |
派生QRunnable,使用QThreadPool::globalInstance()->start() 运行它 |
||
派生QThread,重新实现QThread::run() ,使用QThread::start() 运行它 |
||
一次调用 | 需要操作一个容器中所有的项。使用处理器所有可用的核心。一个常见的例子是从图像列表生成缩略图。 | QtConcurrent 提供了map()函你数来将操作应用到容器中的每一个元素,提供了fitler()函数来选择容器元素,以及指定reduce函数作为选项来组合剩余元素。 |
一次调用 | 一个耗时运行的操作需要放入另一个线程。在处理过程中,状态信息需要发送会GUI线程。 | 使用QThread,重新实现run函数并根据需要发送信号。使用信号槽的queued连接方式将信号连接到GUI线程的槽函数。 |
持久运行 | 生存在另一个线程中的对象,根据要求需要执行不同的任务。这意味着工作线程需要双向的通讯。 | 派生一个QObject对象并实现需要的信号和槽,将对象移动到一个运行有事件循环的线程中并通过queued方式连接的信号槽进行通讯。 |
持久运行 | 生存在另一个线程中的对象,执行诸如轮询端口等重复的任务并与GUI线程通讯。 | 同上,但是在工作线程中使用一个定时器来轮询。尽管如此,处理轮询的最好的解决方案是彻底避免它。有时QSocketNotifer是一个替代。 |
QThread是一个非常便利的跨平台的对平台原生线程的抽象。启动一个线程是很简单的。让我们看一个简短的代码:生成一个在线程内输出"hello"并退出的线程。
// hellothread/hellothread.h
class HelloThread : public QThread
{
Q_OBJECT
private:
void run();
};
我们从QThread派生出一个类,并重新实现run方法。
// hellothread/hellothread.cpp
void HelloThread::run()
{
qDebug() << "hello from worker thread " << thread()->currentThreadId();
}
run方法中包含将在另一个线程中运行的代码。在本例中,一个包含线程ID的消息被打印出来。 QThread::start()
将在另一个线程中被调用。
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
HelloThread thread;
thread.start();
qDebug() << "hello from GUI thread " << app.thread()->currentThreadId();
thread.wait(); // do not exit before the thread is completed!
return 0;
}
QObject有线程关联(thread affinity)[如何翻译?关联?依附性?dbzhang800 20110618],换句话说,它生存于一个特定的线程。这意味着,在创建时QObject保存了到当前线程的指针。当事件使用postEvent()被派发时,这个信息变得很有用。事件被放置到相应线程的事件循环中。如果QObject所依附的线程没有事件循环,该事件将永远不会被传递。
要启动事件循环,必须在run()内调用exec()。线程关联可以通过moveToThread()来更改。
如上所述,当从其他线程调用对象的方法时开发人员必须始终保持谨慎。线程关联不会改变这种状况。 Qt文档中将一些方法标记为线程安全。postEvent()就是一个值得注意的例子。一个线程安全的方法可以同时在不同的线程被调用。
通常情况下并不会并发访问的一些方法,在其他线程调用对象的非线程安全的方法在出现造成意想不到行为的并发访问前数千次的访问可能都是工作正常的。编写测试代码不能完全确保线程的正确性,但它仍然是重要的。在Linux上,Valgrind和Helgrind有助于检测线程错误。
QThread的内部结构非常有趣:
QObject必须始终和parent在同一个线程。对于在run()中生成的对象这儿有一个惊人的后果:
void HelloThread::run()
{
QObject *object1 = new QObject(this); //error, parent must be in the same thread
QObject object2; // OK
QSharedPointer object3(new QObject); // OK
}
互斥量是一个拥有lock()和unlock()方法并记住它是否已被锁定的对象。互斥量被设计为从多个线程调用。如果信号量未被锁定lock()将立即返回。下一次从另一个线程调用会发现该信号量处于锁定状态,然后lock()会阻塞线程直到其他线程调用unlock()。此功能可以确保代码段将在同一时间只能由一个线程执行。
Qt的事件循环对线程间的通信是一个非常有价值的工具。每个线程都可以有它自己的事件循环。在另一个线程中调用一个槽的一个安全的方法是将调用放置到另一个线程的事件循环中。这可以确保目标对象调用另一个的成员函数之前可以完成当前正在运行的成员函数。
那么,如何才能把一个成员调用放于一个事件循环中? Qt的有两种方法来做这个。一种方法是通过queued信号槽连接;另一种是使用QCoreApplication::postEvent()派发一个事件。queued的信号槽连接是异步执行的信号槽连接。内部实现是基于posted的事件。信号的参数放入事件循环后信号函数的调用将立即返回。
连接的槽函数何时被执行依赖于事件循环其他的其他操作。
通过事件循环通信消除了我们使用互斥量时所面临的死锁问题。这就是我们为什么推荐使用事件循环,而不是使用互斥量锁定对象的原因。
以上是转自http://blog.csdn.net/chinabinlang/article/details/35988801
下面说一下我的理解。当你使用Qthread在主线程中创建线程并movetoThread时,那么run函数中的消息机制(主要指的是信号槽机制)是跟随这个次线程(也就是这个qthread对象)。如果你想让run的消息机制跑着主线程中,那么你可以继承QThread并重写run函数即可。
在Qt中,使用线程主要有两种方式。
1、自定义线程类,继承自QThread,并重写run方法即可。该方法就不再阐述了。
2、就是推荐使用的方法。
示例代码如下:
1、自定义Worker类,将线程中的逻辑在该类中以槽函数的方式实现:
Worker.h
#ifndef WORKER_H
#define WORKER_H
#include
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr);
~Worker();
public slots:
void doSomething();
};
#endif // WORKER_H
Worker.cpp
#include “worker.h”
#include
Worker::Worker(QObject *parent) : QObject(parent)
{
}
Worker::~Worker()
{
qDebug()<<“free worker…”;
}
void Worker::doSomething()
{
qDebug()<<“do something…”;
}
2、调用处的代码
QThread *thread = new QThread;
Worker* worker = new Worker;
connect(thread,SIGNAL(started()),worker,SLOT(doSomething()));
connect(thread,SIGNAL(finished()),worker,SLOT(deleteLater()));
connect(thread,SIGNAL(finished()),thread,SLOT(deleteLater()));
worker->moveToThread(thread);
thread->start();
以上代码非常清晰易懂,在线程开始的时候触发Worker的doSomething槽函数,当线程结束的时候调用Worker的deleteLater槽函数以释放new出来的worker对象和thread对象。
但是问题出现了!thread线程永远不会结束!其原因是虽然worker对象的doSomething槽函数结束了,但是thread线程依然处于自己的事件循环中!也就导致了thread和worker的内存泄漏!
正确的处理办法:在worker对象的槽函数doSomething结束的时候,应发射结束信号来间接控制线程!具体代码如下:
#ifndef WORKER_H
#define WORKER_H
#include
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr);
~Worker();
signals:
void finished();//完成信号
public slots:
void doSomething();
};
#endif // WORKER_H
————————————————
版权声明:本文为CSDN博主「晚餐吃什么」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Think88666/article/details/86531118
#include "worker.h"
#include
Worker::Worker(QObject *parent) : QObject(parent)
{
}
Worker::~Worker()
{
qDebug()<<"free worker...";
}
void Worker::doSomething()
{
qDebug()<<"do something...";
emit finished();
}
————————————————
版权声明:本文为CSDN博主「晚餐吃什么」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Think88666/article/details/86531118
QThread *thread = new QThread;
Worker*worker = new Worker;
connect(worker,SIGNAL(finished()),thread,SLOT(quit()));//新增
connect(thread,SIGNAL(started()),worker,SLOT(doSomething()));
connect(thread,SIGNAL(finished()),worker,SLOT(deleteLater()));
connect(thread,SIGNAL(finished()),thread,SLOT(deleteLater()));
worker->moveToThread(thread);
thread->start();
这样就可以保证,在worker对象结束任务时,thread也退出了事件循环并发射finished信号且释放内存!
QT中有多种创建线程的方式,每一种的应用场景和使用方式都有些区别,
使用QThread创建线程是我们最常见的一种方式,步骤如下:
继承QThread
重写run()函数
通过start()函数启动线程
优点:可以通过信号槽与外界进行通信。
缺点:①每次新建一个线程都需要继承QThread,实现一个新类,使用不太方便。
②要自己进行资源管理,线程释放和删除。并且频繁的创建和释放会带来比较大的内存开销。
适用场景:QThread适用于那些常驻内存的任务。
class MyThread : public QThread
{
Q_OBJECT
protected:
void run(){
//do something
qDebug() <<__FUNCTION__ << "id = " << QThread::currentThreadId();
QThread::msleep(1000);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() <<__FUNCTION__ << "id = " << QThread::currentThreadId();
MyThread * myThread = new MyThread;
myThread->start();
return a.exec();
}
继承QRunnable。和QThread使用一样, 首先需要将你的线程类继承于QRunnable。
重写run函数。还是和QThread一样,需要重写run函数,run是一个纯虚函数,必须重写。
使用QThreadPool启动线程
优点:无需手动释放资源,QThreadPool启动线程执行完成后会自动释放。
缺点:不能使用信号槽与外界通信。
适用场景:QRunnable适用于线程任务量比较大,需要频繁创建线程。QRunnable能有效减少内存开销。
————————————————
版权声明:本文为CSDN博主「luoyayun361」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/luoyayun361/article/details/97150788
概述
说到线程通常会想到QThread,但其实Qt中创建线程的方式有多种,这里主要介绍其中一种QRunnable,QRunnable和QThread用法有些不同,并且使用场景也有区别。接下来就来看看QRunnable的用法、使用场景以及注意事项。
用法
要使用QRunnable创建线程,步骤如下:
继承QRunnable。和QThread使用一样, 首先需要将你的线程类继承于QRunnable。
重写run函数。还是和QThread一样,需要重写run函数,run是一个纯虚函数,必须重写。
使用QThreadPool启动线程
和QThread的区别
与外界通信方式不同。由于QThread是继承于QObject的,但QRunnable不是,所以在QThread线程中,可以直接将线程中执行的结果通过信号的方式发到主程序,而QRunnable线程不能用信号槽,只能通过别的方式,等下会介绍。
启动线程方式不同。QThread线程可以直接调用start()函数启动,而QRunnable线程需要借助QThreadPool进行启动。
资源管理不同。QThread线程对象需要手动去管理删除和释放,而QRunnable则会在QThreadPool调用完成后自动释放。
#include
#include
class CusRunnable : public QRunnable
{
public:
explicit CusRunnable();
~CusRunnable();
void run();
};
#endif // CUSRUNNABLE_H
#include "cusrunnable.h"
#include
#include
CusRunnable::CusRunnable()
{
}
CusRunnable::~CusRunnable()
{
qDebug() << __FUNCTION__;
}
void CusRunnable::run()
{
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QThread::msleep(1000);
}
然后在主界面中调用该线程。
#include
#include "cusrunnable.h"
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
private:
CusRunnable * m_pRunnable = nullptr;
};
#endif // WIDGET_H
#include "widget.h"
#include
#include
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
m_pRunnable = new CusRunnable(this);
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QThreadPool::globalInstance()->start(m_pRunnable);
}
Widget::~Widget()
{
qDebug() << __FUNCTION__ ;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6V7GqUsH-1616343294976)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1616341615538.png)]
描述
moveToThread是在QThread的用法基础上扩展出来的,它是QObject的接口,相对QThread线程方式来说,moveToThread使用更灵活,不需要继承QThread,也不用重写run函数。只需要将一个继承于QObject的类通过moveToThread移到QThread的一个对象中。
需要注意的是:
只有在槽中执行的操作才是在线程中执行的,所以需要通过连接信号槽的方式来实现
如果object对象存在父对象,不能将其移到子线程中执行。
#include
#include
class Command : public QObject
{
Q_OBJECT
public:
explicit Command(QObject *parent = nullptr);
void sendMessage(const QString &msg);
signals:
void sigMsg(QString msg);
private slots:
void onMessage(QString msg);
private:
QThread * m_pThread = nullptr;
};
#include "command.h"
#include
Command::Command(QObject *parent) :
QObject(parent)
{
m_pThread = new QThread();
this->moveToThread(m_pThread);
connect(this,&Command::sigMsg,this,&Command::onMessage);
m_pThread->start();
qDebug()<< __FUNCTION__ << " id = "<< QThread::currentThreadId();
}
void Command::sendMessage(const QString &msg)
{
emit sigMsg(msg);
}
void Command::onMessage(QString msg)
{
qDebug()<< __FUNCTION__ << " id = "<< QThread::currentThreadId();
}
重点:只有在槽中执行的操作才是线程执行的操作,所以需要通过信号操作的方式,
调用:
m_pCommand = new Command();//不能指定父类
m_pCommand->sendMessage("ABC");//发送信号
有关QtConcurrent::run的使用方法在之前的文章里面有详细介绍过,在这里,Concurrent是并发的意思,QtConcurrent是一个命名空间,提供了一些高级的 API,使得在编写多线程的时候,无需使用低级线程原语,如读写锁,等待条件或信号。使用QtConcurrent编写的程序会根据可用的处理器内核数自动调整使用的线程数。这意味着今后编写的应用程序将在未来部署在多核系统上时继续扩展。
QtConcurrent::run能够方便快捷的将任务丢到子线程中去执行,无需继承任何类,也不需要重写函数,使用非常简单。详见前面的文章介绍,这里不再赘述。
需要注意的是,由于该线程取自全局线程池QThreadPool,函数不能立马执行,需要等待线程可用时才会运行。
下面不是完整的列表,不过稍微思考一下,你就能猜出那些类需要消息循环了。
阻塞消息循换
在讨论为什么我们不应该阻塞消息循换之前,先说明一下“阻塞”的含义是什么。想像一下,有一个在点击时可以发送信号的按钮,信号绑定到我们的工作类对象的一个槽函数上,这个槽函数会做很多工作。当你点击按钮时,函数调用栈看起来应该像下面这样(栈底在上):
main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()
在 main() 函数中,我们通过调用 QApplication::exec() (第2行) 启动了一个消息循换。窗口管理器发送一个鼠标点击的事件,Qt 内核会得到这个消息,然后转化为一个 QMouseEvent 对象,通过 QApplication::notify()(此处没有列出)函数发送给 widget 的 event() 函数(第4行)。如果按钮没有重写 event() 函数,那么他的基类(QWidget)实现的 event() 函数会被调用。QWidget::event() 检测到鼠标点击事件,然后调用相应的事件处理函数,就是上面代码中的 Button::mousePressEvent()(第5行)函数。我们重写了这个函数,让他发送一个 Button::clicked() 信号(第6行),这个信号会调用 Worker 类对象的槽函数 Worker::doWork() (第8行)。
当 Worker 对象正在忙于工作的时候,消息循换在做什么?我们可能会猜测:什么也不做!消息循换分发了鼠标点击事件然后等待,等待消息处理者返回。我们阻塞了消息循换,这意味在槽函数 doWork() 返回之前,不会再有消息被分发出去,消息会不断进入消息队列而不能的得到及时的处理。
当事件分发被卡住的时候,窗口不会刷新(QPaintEvent 对象在消息队列中),不能响应其他的交互行为(和前面的原因一样),定时器超时事件不会触发、网络通信变慢然后停止。此外,很多窗口管理器会检测到你的程序不再处理事件,而提示程序无响应。这就是为什么迅速的处理事件然后返回消息循环如此重要的原因。
强制分发事件
那么,如果有一个耗时的任务同时我们又不想阻塞消息循换,这时该如何去做?一个可能的回答是:把这个耗时的任务移动到其他的线程中:下一节中我们可以看到如何做。我们还有一个可选的办法,那就是在我们耗时的任务中通过调用 QCoreApplication::processEvents() 来手动强制跑起消息循换。QCoreApplication::processEvents() 会处理所有队列上的事件然后返回。
另一个可选的方案,我们可以利用 QEventLoop [doc.qt.nokia.com] 强制再加入一个消息循环。通过调用 QEventLoop::exec() 函数,我们加入一个消息循换,然后连接一个信号到 QEventLoop::quit() 槽函数上,来让循环退出。例如:
QNetworkAccessManager qnam;
QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(...)));
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
/* reply has finished, use it */
QNetworkReply 不提供阻塞的接口,同时需要一个消息循环。我们进入了一个局部的 QEventLoop,当 reply 发出 finished 信号时,这个事件循环就结束了。
通过“其他路径”重入消息循换时需要特别小心:这可能导致不期望的递归!回到刚才的按钮例子中。如果我们再槽函数 doWork() 中调用 QCoreApplication::processEvents() ,同时用户再次点击了按钮,这个槽函数 doWork() 会再一次被调用:
main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork() // first, inner invocation
QCoreApplication::processEvents() // we manually dispatch events and…
[…]
QWidget::event(QEvent * ) // another mouse click is sent to the Button…
Button::mousePressEvent(QMouseEvent *)
Button::clicked() // which emits clicked() again…
[…]
Worker::doWork() // DANG! we’ve recursed into our slot.
一个快速简单的规避办法是给 QCoreApplication::processEvents() 传入一个参数 QEventLoop::ExcludeUserInputEvents,它会告诉消息循换不要分发任何用户输入的事件(这些事件会停留在队列中)。
幸运的是,同样的问题不会出现在删除事件中(调用 QObject::deleteLater() 会发送该事件到事件队列中)。事实上,Qt 使用了特别的办法来处理它,当消息循环比 deleteLater 调用发生的消息循环更外层时,删除事件才会被处理。例如:
QObject *object = new QObject;
object->deleteLater();
QDialog dialog;
dialog.exec();
这不会导致 object 空悬指针(QDialog::exec() 中的消息循环,比 deleteLater 调用发生的地方层次更深)。同样的事情也会发生在 QEventLoop 启动的消息循环中。我只发现过一个例外(在 Qt 4.7.3 中),如果在没有任何消息循环的时候调用了 deleteLater,那么第一个启动的消息循环会处理这个消息,删除该对象。这是很合理的,因为 Qt 知道不会有任何会执行删除动作的“外层”循环,因此会立即删除该对象
到现在为止,我们已经讨论过“消息循环”,但讨论的仅仅是在一个 Qt 应用程序中只有一个消息循换的情况。但不是下面这种情况:QThread 对象可以启动一个自己代表的线程中的消息循换。因此,我们把在 main() 函数中通过调用 QCoreApplication::exec()(该函数只能在主线程中调用)启动的消息循换叫做主消息循环。它也叫做 GUI 线程,因为 UI 相关的操作只能(应该)在该线程中执行。一个 QThread 局部消息循换可以通过调用 QThread::exec() 来启动(在 run() 函数中):
class Thread : public QThread {
protected:
void run() {
/* ... initialize ... */
exec();
}
};
上面我们提到,从 Qt 4.4 开始,QThread::run() 不再是一个纯虚函数,而是默认调用 QThread::exec()。和 QCoreApplication 一样,QThread 也有 QThread::quit() 和 QThread::exit() 函数,来停止消息循换。
一个线程的消息循环为所有在这个线程中的 QObject 对象分发消息;默认的,它包括所有在这个线程中创建的对象,或者从其他线程中移过来的对象(接下来详细说明)。同时,一个 QObject 对象的线程相关性是确定的,也就是说这个对象生存在这个线程中。这个适用于在 QThread 对象的构造函数中创建的对象:
class MyThread : public QThread
{
public:
MyThread()
{
otherObj = new QObject;
}
private:
QObject obj;
QObject *otherObj;
QScopedPointer yetAnotherObj;
};
在创建一个 MyThread 对象之后,obj,otherObj,yetAnotherObj 的线程相关性如何?我们必须看看创建这些对象的线程:它是运行 MyThread 构造函数的线程。因此,所有这三个对象都不属于 MyThread 线程,而是创建了 MyThread 对象的线程(MyThread 对象也属于该线程)。
我们可以使用线程安全的 QCoreApplication::postEvent() 函数来给对象发送事件。它会把事件放入该对象所在消息循环的事件队列中;因此,只有这个线程有消息循环,消息才会被分发。
理解 QObject 和它的子类不是线程安全的(虽然它是可重入的)这非常重要;由于它不是线程安全的,所以你不能同时在多个线程中同时访问同一个 QObject 对象,除非你自己串行化了所有对这些内部数据的访问(比如使用了互斥量来保护内部数据)。记住当你从其他线程访问 QObject 对象时,这个对象有可能正在处理它所在的消息循环分发给它的事件。同样的,你也不能从另一个线程中删除一个 QObject 对象,而必须使用 QObject::deleteLater() 函数,它会发送一个事件到对象所在线程中,然后在该线程中删除对象。
此外,QWidget 和它的所有子类,还有其他的 UI 相关类(非 QObject 子类,比如 QPixmap)还是不可重入的:他们仅仅可以在 UI 线程中使用。
我们可以通过调用 QObject::moveToThread() 来改变 QObject 对象和线程之前的关系,它会改变对象本身以及它的孩子与线程之前的关系。由于 QObject 不是线程安全的,所以我们必须在它所在的线程中使用;也就是说,你仅仅可以在他们所处的线程中把它移动到另一个线程去,而不能从其他线程中把它从所在的线程中移动过来。而且,Qt 要求一个 QObject 对象的汉子必须和他的父亲在同一个线程中,也就是说:
class Thread : public QThread {
void run() {
QObject *obj = new QObject(this); // WRONG!!!
}
};
这是因为 QThread 对象所在的线程是另外的线程,即,QThread 对象所在的线程是创建它的线程。
Qt 要求所有在线程中的对象必须在线程结束之前销毁;利用 QThread::run() 函数,在该函数中仅创建栈上的对象,这一点可以很容易的做到。
跨线程信号槽
有了这些前提,我们如何调用另一个线程中 QObject 对象的函数?Qt 提供了一个非常漂亮和干净的解决方案:我们发送一个事件到线程的消息队列中,事件的处理,将调用我们感兴趣的函数(当然这个线程需要启动一个事件循环)。该设施围绕 Qt 的元对象编译器(MOC)提供的方法内省而构建:因此,信号,槽,函数,只要使用了 Q_INVOKABLE 宏,那么就可以从另外的线程调用它。
QMetaObject::invokeMethod() 静态方法为我们实现了这个功能:
QMetaObject::invokeMethod(object, "methodName",
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
注意,由于参数需要在消息传递时拷贝,这些类型的参数需要提供公有的构造函数,析构函数和拷贝构造函数,而且要使用 qRegisterMetaType() 函数将类型注册到 Qt 类型系统中。
跨线程的信号槽工作方式是类似的。当我们将信号和曹连接时,QObject::connect 函数的第5个参数可以指定连接的类型:
direct connection:意思是槽函数会在信号发送的线程中直接被调用。
queued connection:意思是事件会发送到接收者所在线程的消息队列中,消息循环会稍后处理该事件然后调用槽函数。
blocking queued connection:和 queued connection 类似,但是发送线程会阻塞,直到接收者所在线程的消息循环处理了该事件,调用了槽函数之后,才会返回;
在任何情况下,记住发送者所在的线程一点都不重要!在自动连接的情况下,Qt 会检查信号调用的线程,然后与接收者所在线程比较,然后决定使用哪种连接类型。特别的,Threads and QObjects[doc.qt.nokia.com] (4.7.1) 在下面的情况下是错误的:
动连接(默认值),如果发送者和接收者在同一线程它和直接连接(direct connection)的行为是一样的;如果发送者和接收者在不同的线程它和队列连接(queued connection)的行为是一样的。
因为发送者所在的线程和无关紧要的。例如:
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 对象在同一个线程中,再次强调,发送者所在线程是无关紧要的),这时将使用 queued connection。
另一个常见的陷阱:
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() 信号时,将会使用哪种连接类型?你应该已经猜到了:direct connection。这是因为 Thread 对象所在线程就是信号发送的线程。在槽函数 aSlot() 中,我们可能访问 Thread 类的成员,而同时 run() 函数可能也在访问,他们会同时进行:这是完美的灾难配方。
另一个例子,或许也是最重要的一个:
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* ... */
}
protected:
void run() {
QObject *obj = new Object;
connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));
/* ... */
}
};
在上面的情形中,连接类型是 queued connection,因此你需要在 Thread 对象所在线程启动一个消息循环。
下面是一个你经常可以在论坛、博客或其他地方看到的解决方案。那就是在 Thread 的构造函数中增加一个
class Thread : public QThread {
Q_OBJECT
public:
Thread() {
moveToThread(this); // WRONG
}
/* ... */
};
这确实可以工作(因为现在线程对象所在的线程的确改变了),但是这是个非常糟糕的设计。错误在于我们误解了 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();
应该做&不应该做
你可以…
你不应该…
禁止…
什么时候应该使用线程?
当使用阻塞 API 时
如果你需要使用没有提供非阻塞API的库(例如信号槽,事件,回调函数,等),那么避免阻塞消息循环的唯一解决方案就是开启一个进程或线程。由于创建一个工作进程,让它完成任务并通过进程通信返回结果与开启一个线程相比是困难并且昂贵的,所以创建一个线程是更普遍的做法。
地址解析(只是举个例子,不是在讨论蹩脚的第三方 API。这是每一个 C 语言函数库中包含的东西)就是一个很好的例子,它把主机名转换为地址。它会调用域名解析系统(DNS)来查询。虽然一般情况下,它会立即返回,但是远程服务器有可能故障,有可能丢包,有可能网络突然中断,等等。简而言之,它可能需要等待很长时间才相应我们发出的请求。
UNIX 系统中的标准 API 是阻塞的(不仅仅是旧的 API gethostbyname(3),新的更好的 getservbyname(3) 和 getaddrinfo(3) 也是一样)。QHostInfo [doc.qt.nokia.com] 是处理主机名解析的 Qt 类,它使用 QThreadPool 来使得请求在后台运行(看这里 [qt.gitorious.com];如果线程支持被关闭的话,它会切换为阻塞方式)。
另一个简单的例子是图像加载和缩放。QImageReader [doc.qt.nokia.com] 和 QImage[doc.qt.nokia.com] 只提供阻塞方法来从设备读取图像,或改变图像的分辨率。如果你正在处理非常大的图像,这些操作可能会花费数十秒。
当你想要充分利用多CPU时
多线程可以让你的程序更好的利用多处理器系统。每个线程是由操作系统独立调用的,如果你的程序运行在这样的机器上,线程调度就可以让多个处理器同时运行不同的线程。
比如,考虑一个批量生成缩略图的程序。一个有 n 个线程的线程农场(有固定线程数目的线程池),n 是系统中可用 CPU 的数量(可参考 QThread::idealThreadCount()),它可以将处理任务分布到多个cpu上,这样我们就可以获得与cpu数量有关的效率线性增长(简单的,我们把CPU考虑为瓶颈)。
当你不想被阻塞时
呃…从一个例子开始会更好。
这是一个高级话题,你可以暂时忽略。Webkit 中的 QNetworkAccessManager 是一个很好的例子。Webkit 是一个流行的浏览器引擎,它是处理网页布局和显式的一组类的集合,Qt 中 QwebView 类使用了它。
QNetworkAccessManager 是 Qt 中处理 HTTP 请求和响应的类,我们可以把它当作浏览器的引擎。Qt 4.8 之前,它没有使用任何工作线程;所有的处理都在 QNetworkAccessManager 和 QNetworkReply 所在的同一个线程。
虽然在网络通信中使用线程是一个好办法,但是它也存在问题:如果你没有尽快从 socket 中读取数据,内核缓冲会被其他数据填充,数据包将被丢掉,可想而知,数据传输速率将下降。
socket 活动(也就是 socket 是否可读)是由 Qt 的事件循环还管理的。阻塞事件循环会导致传输性能下降,因为这时没有人会被告知现在数据已经可读(所以没有人会去读取数据)。
但是什么会阻塞消息循环?可悲的是:WebKit 自己阻塞了消息循环。一旦消息可读,Webkit 开始处理网页布局。不幸的是,这个处理是复杂而昂贵的,它会阻塞消息循换一(小)会儿,但足以影响传输效率(宽带连接这里起到了作用,在短短几秒内就可填满内核缓存)。
总结一下,这个过程发生的事情:
整个页面的加载时间由于 Webkit 自己引起的问题而变得很慢。
注意,由于 QNetworkAccessManager 和 QNetworkReply 都是 QObject,它们都不是线程安全的,因此你不能将它移动到另一个线程然后继续在你的线程中继续使用它,因为你可能从两个线程中同时访问它:你自己的线程和它所在的线程,因为它所在的消息循环会将事件分发给它处理。
在 Qt 4.8 中,QNetworkAccessManager 现在默认使用单独的线程处理 HTTP 请求,因此 UI 反应慢和系统缓冲被填充过快的问题得以解决。
什么时候不应该使用线程?
计时器
这可能是最糟糕的线程滥用。如果你不得不重复调用一个方法(例如,每秒调用一次),很多人会这么做:
// VERY WRONG
while (condition) {
doWork();
sleep(1); // this is sleep(3) from the C library
}
然后会发现这阻塞了事件循环,然后决定使用线程来解决:
// WRONG
class Thread : public QThread {
protected:
void run() {
while (condition) {
// notice that "condition" may also need volatiness and mutex protection
// if we modify it from other threads (!)
doWork();
sleep(1); // this is QThread::sleep()
}
}
};
一个更好更简单的办法是使用计时器,一个超时时间为1秒的 QTimer [doc.qt.nokia.com] 对象,和 doWork() 槽函数:
class Worker : public QObject
{
Q_OBJECT
public:
Worker() {
connect(&timer, SIGNAL(timeout()), this, SLOT(doWork()));
timer.start(1000);
}
private slots:
void doWork() {
/* ... */
}
private:
QTimer timer;
};
我们所需要做的就是启动一个消息循环,然后 doWork() 函数会每一秒调用一次。
网络通信/状态机
下面是一个非常常见的网络通信的设计:
socket->connect(host);
socket->waitForConnected();
data = getData();
socket->write(data);
socket->waitForBytesWritten();
socket->waitForReadyRead();
socket->read(response);
reply = process(response);
socket->write(reply);
socket->waitForBytesWritten();
/* ... and so on ... */
不用多说,这些 waitFor*() 函数调用会阻塞消息循环,冻结 UI,等等。注意,上面的代码没有任何的错误处理,不然它会更繁琐。上面的错误在于我们忘记了最初网络设计的就是异步的,如果我们使用同步处理,那就是朝自己的脚开枪。解决上面的问题,许多人会简单的把它移动到不同的线程中。
result = process_one_thing();
if (result->something())
process_this();
else
process_that();
wait_for_user_input();
input = read_user_input();
process_user_input(input);
/* ... */
它和上面网络的例子有着同样的陷阱。
让我们退一步,从更高的视角来看看我们构建的东西,我们构建了一个状态机来处理输入。
等等。
现在,我们有很多办法来构建一个状态机(Qt 就为我们提供了一个可使用的类:QStateMachine[doc.qt.nokia.com]),最简单的办法就是使用枚举(整型)来记录当前的状态。我们可以重写上面的代码:
class Object : public QObject
{
Q_OBJECT
enum State {
State1, State2, State3 /* and so on */
};
State state;
public:
Object() : state(State1)
{
connect(source, SIGNAL(ready()), this, SLOT(doWork()));
}
private slots:
void doWork() {
switch (state) {
case State1:
/* ... */
state = State2;
break;
case State2:
/* ... */
state = State3;
break;
/* etc. */
}
}
};
“source” 对象和“ready()”信号是什么?我们想要的是:拿网络例子来说,我们想要把 QAbstractSocket::connected() 和 QIODevice::readyRead() 连接到我们的槽函数上。当然,如果再多些槽函数更好的话,我们也可以增加更多(比如错误处理的槽函数,由 QAbstractSocket::error() 信号来发起)。这是真正的异步,信号驱动的设计!
把任务分解成小块
想想一下我们有个很耗时但是无法移动到其它线程的任务(或者根本不能移动到其它线程,因为它可能必须在 UI 线程中执行)。如果我们把任务分解成小块,那么我们就可以返回消息循环,让消息循环分发事件,然后让它调用处理后续任务块的函数。如果我们还记得 queued connection 如何实现的话,那就很容易解决这个问题了:事件发送到接收者所在的事件循环中,当事件被分发的时候,相应的槽函数被调用。
我们可以使用 QMetaObject::invokeMethod() 函数,用参数 Qt::QueuedConnection 指定连接类型,来实现这个功能;这需要函数可调用,也就是说函数必须是个槽函数或者使用了 Q_INVOKABLE 宏修饰。如果我们还要给函数传递参数,那么我们要保证参数类型已经通过函数 qRegisterMetaType() 注册到了 Qt 的类型系统中。下面的代码给我们展示了这种做法:
class Worker : public QObject
{
Q_OBJECT
public slots:
void startProcessing()
{
processItem(0);
}
void processItem(int index)
{
/* process items[index] ... */
if (index < numberOfItems)
QMetaObject::invokeMethod(this,
"processItem",
Qt::QueuedConnection,
Q_ARG(int, index + 1));
}
};
因为这里没有线程调用,所以它可以很容易的暂停/恢复/取消任务,也可以很容易的得到计算结果。
/* ... */
state = State2;
break;
case State2:
/* ... */
state = State3;
break;
/* etc. */
}
}
};
“source” 对象和“ready()”信号是什么?我们想要的是:拿网络例子来说,我们想要把 QAbstractSocket::connected() 和 QIODevice::readyRead() 连接到我们的槽函数上。当然,如果再多些槽函数更好的话,我们也可以增加更多(比如错误处理的槽函数,由 QAbstractSocket::error() 信号来发起)。这是真正的异步,信号驱动的设计!
**把任务分解成小块**
想想一下我们有个很耗时但是无法移动到其它线程的任务(或者根本不能移动到其它线程,因为它可能必须在 UI 线程中执行)。如果我们**把任务分解成小块**,那么我们就可以返回消息循环,让消息循环分发事件,然后让它调用处理后续任务块的函数。如果我们还记得 queued connection 如何实现的话,那就很容易解决这个问题了:事件发送到接收者所在的事件循环中,当事件被分发的时候,相应的槽函数被调用。
我们可以使用 QMetaObject::invokeMethod() 函数,用参数 Qt::QueuedConnection 指定连接类型,来实现这个功能;这需要函数可调用,也就是说函数必须是个槽函数或者使用了 Q_INVOKABLE 宏修饰。如果我们还要给函数传递参数,那么我们要保证参数类型已经通过函数 qRegisterMetaType() 注册到了 Qt 的类型系统中。下面的代码给我们展示了这种做法:
class Worker : public QObject
{
Q_OBJECT
public slots:
void startProcessing()
{
processItem(0);
}
void processItem(int index)
{
/* process items[index] ... */
if (index < numberOfItems)
QMetaObject::invokeMethod(this,
"processItem",
Qt::QueuedConnection,
Q_ARG(int, index + 1));
}
};
因为这里没有线程调用,所以它可以很容易的暂停/恢复/取消任务,也可以很容易的得到计算结果。