我们要实现的是读取大文件qtgui.index
的内容加入文本框中。预览
很容易想到的方法:
1 2 3 4 5 6 7 8 9 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又有三种方法:信号与槽实现线程间通信、元对象系统实现线程间通信、分离线程与任务。前两种也是跨线程调用函数的方法。
使用QThread 信号与槽实现线程间通信 这是线程间通信比较常用的方法。代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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线程的信号与槽机制这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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所在行换成:
1 2 QMetaObject::invokeMethod (m_obj,"appendText" ,Qt::AutoConnection, Q_ARG (QString,line) );
在构造函数中直接运行线程:
1 2 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
("MyClass")`来注册这个类型。
我们知道QWidget及其子类都是不可重入的,也就是**GUI类只能在GUI线程使用**,本例中如果在子线程直接调用`appendText`,可能也能得到正确结果,但这种做法并不正确。
另外我们无法在类外调用private成员函数。本例中我们可以**在子线程使用MainWindow的私有方法**,只要把`appendText`改为带`Q_INVOKABLE`修饰的私有成员函数即可。
### moveToThread分离线程与任务
一个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对象自己作为它的父对象。比如这种做法是错的:
1 2 3 4 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的子类,因为它不可重入:
1 2 3 4 5 6 7 8 9 10 class MyObj : public QObject{ Q_OBJECT public : MyObj (); signals: void toLine (QString line) ; private slots: void doWork () ; };
源文件的代码,主要是槽函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 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在头文件里声明:
1 2 3 4 5 6 7 8 9 10 11 t = new 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 ()) );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线程
QueuedConnection的线程情况:QueuedConnection
DirectConnection 的线程情况:DirectConnection
AutoConnection 的线程情况:AutoConnection
同步调用:发出信号后,当前线程等待槽函数执行完毕后才继续执行。 异步调用:发出信号后,立即执行剩下逻辑,不关心槽函数什么时候执行。
总结:
参考:Qt信号槽的一些事 Qt线程使用杂谈