open(QIODevice::ReadOnly); QTextStream *stream = " type="text/javascript">
频道栏目
首页 > 资讯 > 其他 > 正文

Qt中多线程的使用(一)

18-07-21        来源:[db:作者]  
收藏   我要投稿

我们要实现的是读取大文件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又有三种方法:信号与槽实现线程间通信、元对象系统实现线程间通信、分离线程与任务。前两种也是跨线程调用函数的方法。

使用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("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对象自己作为它的父对象。比如这种做法是错的:

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:"<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:"<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

同步调用:发出信号后,当前线程等待槽函数执行完毕后才继续执行。
异步调用:发出信号后,立即执行剩下逻辑,不关心槽函数什么时候执行。

总结:

相关TAG标签
上一篇:vmware -centos6.5静态IP配置的步骤教程
下一篇:linux系统下iotable_init 静态映射与内核页表的建立实例教程
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站