Qt中多线程的使用(一)

我们要实现的是读取大文件qtgui.index的内容加入文本框中。
Qt中多线程的使用(一)_第1张图片
很容易想到的方法:

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

QueuedConnection的线程情况:
QueuedConnection

DirectConnection 的线程情况:
Qt中多线程的使用(一)_第2张图片

AutoConnection 的线程情况:
AutoConnection

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

总结:
Qt中多线程的使用(一)_第3张图片

参考:Qt信号槽的一些事
Qt线程使用杂谈

你可能感兴趣的:(Qt)