我们要实现的是读取大文件qtgui.index
的内容加入文本框中。
很容易想到的方法:
QFile* file = new QFile("E:\qtgui.index");
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);
while(!stream->atEnd())
{
QString line = stream->readLine();
ui->textEdit->append(line);
}
结果运行后发现程序失去响应。因为读取大文件要很长时间,事件循环一直等待函数返回,这样导致阻塞事件循环。结果,GUI线程所有的绘制和交互都被阻塞在事件队列中,无法执行重绘等事件,整个程序就失去响应了。
解决阻塞一般有两种方法:
在任务中不断调用QCoreApplication::processEvents()手动强制事件循环,它会在处理完队列中所有事件后返回。但是如果两次函数调用的间隔时间不够短,用户仍能明显感觉到程序卡顿。所以在while循环最后加一行QApplication::processEvents();
即可。
Qt提供了三种方式:QThread、QRunnable / QThreadPool、QtConcurrent。其中最常用的是 QThread。
对于本例,使用QThread又有三种方法:信号与槽实现线程间通信、元对象系统实现线程间通信、分离线程与任务。前两种也是跨线程调用函数的方法。
这是线程间通信比较常用的方法。代码:
class ReadThread : public QThread
{
Q_OBJECT
public:
ReadThread(QObject* obj);
signals:
void toLine(QString line);
protected:
void run() Q_DECL_OVERRIDE;
private:
QFile* file;
QObject* m_obj;
};
ReadThread::ReadThread(QObject* obj):
m_obj(obj)
{
file = new QFile("E:\qtgui.index");
}
void ReadThread::run()
{
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);
while(1)
{
while(!stream->atEnd())
{
QString line = stream->readLine();
emit toLine(line);
QThread::msleep(15);
}
}
}
需要把读取任务放到run()
里,构造函数要传入GUI类的指针
在GUI线程的信号与槽机制这样:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ReadThread* thread = new ReadThread(this);
thread->start();
connect(thread,SIGNAL(toLine(QString)),this,SLOT(appendText(QString)) );
connect(thread,SIGNAL(finished()),this,SLOT(FinishThread()) );
}
void MainWindow::appendText(QString lineTemp)
{
ui->textEdit->append(lineTemp);
}
其中appendText
是MainWindow的槽函数,Q_ARG的两个形参分别为槽函数的形参类型和实参。
在使用invokeMethod
方法后,使用了QThread的静态函数msleep,因为读取的文件太大,每读取一行就要更新GUI,太耗资源,会导致GUI忙不过来,读一行后稍微休息一下,否则也会阻塞GUI。
QThread的子类一般不定义槽函数,这是不安全的,可能造成主线程和子线程同时访问它,除非使用mutex保护。 但可以定义signal,而且可以在run函数中发射, 因为信号发射是线程安全的。
将ReadThread类中的信号去掉,再把run()中的emit所在行换成:
QMetaObject::invokeMethod(m_obj,"appendText",Qt::AutoConnection,
Q_ARG(QString,line) );
在构造函数中直接运行线程:
ReadThread* thread = new ReadThread(this);
thread->start();
QMetaObject::invokeMethod()
中的第三个参数是信号与槽的连接方式,如果目标进程与当前线程相同则用Qt::DirectConnection
;不同则用Qt::QueuedConnection
,想对象所在线程发送事件,进入目标线程的事件循环;如果是Qt::AutoConnection
,则会根据线程的情况自动判断。 显然这里可以用后两者。
这一机制依赖Qt内省机制,所以只有信号、槽、带Q_INVOKABLE
关键字的成员函数才能使用此方法,本例的appendText为槽函数。
本例的函数形参类型是QString,但如果所调函数的参数类型不是内建数据类型、不属于 QVariant,会报错,即该类型的参数无法进入信号队列。这时需要我们在类的声明之后调用宏Q_DECLARE_METATYPE(MyClass)
,当然前提是该类提供了公有的构造函数、拷贝构造函数和析构函数,并且要在跨线程通信前使用函数 qRegisterMetaType
来注册这个类型。
我们知道QWidget及其子类都是不可重入的,也就是GUI类只能在GUI线程使用,本例中如果在子线程直接调用appendText
,可能也能得到正确结果,但这种做法并不正确。
另外我们无法在类外调用private成员函数。本例中我们可以在子线程使用MainWindow的私有方法,只要把appendText
改为带Q_INVOKABLE
修饰的私有成员函数即可。
一个QObject的线程依附性(thread affinity)是指该对象驻足(live in)在某个线程内。在任何时间都可以通过调用QObject::thread()来查询线程依附性,它适用于在QThread对象构造函数中构建的对象。
一个线程的事件循环为驻足在该线程中的所有QObjects派发了所有事件,其中包括在这个线程中创建的所有对象,或是移植到这个线程中的对象。一个QThread的局部事件循环可以通过调用QThread::exec() 来开启(它包含在run()方法的内部)
将计算任务和线程管理分离,即在一个 QObject 中处理任务,并使用 QObject::moveToThread 改变QObject的依附性。因为QObject不是线程安全的,我们必须在对象所驻足的线程(一般是GUI线程)中使用此函数;也就是说,你只能将对象从它所驻足的线程中推送到其他线程中,而不能从其他线程中拉回来。
QThread**所依附的线程,就是执行QThread * t=new QThread()
的线程,一般就是GUI线程。QThread管理的线程**,就是 run 启动的线程,也就是子线程。线程ID只能在run()函数中调用QThread::currentThreadId()
查看。
此外,Qt要求一个QObject的孩子必须与它们的父对象驻足在同一个线程中。这意味着:不能使用QObject::moveToThread()作用于有父对象的对象; 千万不要在一个线程中创建对象的同时把QThread对象自己作为它的父对象。比如这种做法是错的:
class MyThread : public QThread {
void run() {
QObject* obj = new QObject(this);
} };
然后在GUI线程的构造函数里创建MyThread对象,运行线程后会报错: QObject: Cannot create children for a parent that is in a different thread.
(Parent is MyThread(0x2d07e70), parent’s thread is QThread(0x2cea418)
也就是说MyThread对象在GUI线程,而obj在子线程。
moveToThread底层是依赖Qt事件循环实现的(QCoreApplication::postEvent),所以使用moveToThread必须是在开启Qt事件循环的程序中,就是main函数中调用QCoreApplication::exec的程序。
自定义QObject的子类MyObj,注意不能是QWidget的子类,因为它不可重入:
class MyObj : public QObject
{
Q_OBJECT
public:
MyObj();
signals:
void toLine(QString line);
private slots:
void doWork();
};
源文件的代码,主要是槽函数:
void MyObj::doWork()
{
QFile* file = new QFile("E:\qtgui.index");
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);
qDebug()<<"do work's thread:"<<QThread::currentThread();
while(!stream->atEnd())
{
QString line = stream->readLine();
emit toLine(line);
QThread::msleep(15);
}
}
槽函数实现读取任务,方法与2.1.1类似,关键是信号toLine
和休眠函数。
mainwindow.cpp的代码,私有变量t和obj在头文件里声明:
t = new QThread(); //QThread
obj = new MyObj();
obj->moveToThread(t);
qDebug()<<"main thread:"<<QThread::currentThread();
connect(t,SIGNAL(started()), obj, SLOT(doWork()));
connect(obj,SIGNAL(toLine(QString)),this,SLOT(appendText(QString) ) );
connect(t,SIGNAL(finished()), obj, SLOT(deleteLater()) );
//connect(this,SIGNAL(closeMe()), t, SLOT(terminate()) );
t->start();
第一个connect是启动线程t后,执行任务处理的槽函数;第二个connect是obj执行中发出信号后,文本框添加文本;第三个connect是等线程t结束时,删除obj指针;启动线程t后就可以读取文件并刷新GUI了。
停止子线程的方法最好是给while循环添加布尔量做控制,以及t->quit(); t->wait();
。
注意: 发出信号toLine的obj和this不是同一个线程。
代码中的默认connect类型是Qt::AutoConnection,如果在一个线程就是Qt::DirectConnection,不在一个线程就是Qt::QueuedConnection;
如果是Qt::DirectConnection
,相当于直接调用槽函数,但是当信号发出的线程和槽的对象不在同一个线程的时候,槽函数是在发出的信号中执行的。所以appendText在子线程。
如果是Qt::QueuedConnection
,线程安全,内部通过postEvent实现的。不是实时调用的,槽函数永远在槽函数对象所在的线程中执行。所以appendText在GUI线程
同步调用:发出信号后,当前线程等待槽函数执行完毕后才继续执行。
异步调用:发出信号后,立即执行剩下逻辑,不关心槽函数什么时候执行。
参考:Qt信号槽的一些事
Qt线程使用杂谈