在Qt(C++)中使用QThread实现多线程

1. 引言

多线程对于须要处理耗时任务的应用颇有用,一方面响应用户操做、更新界面显示,另外一方面在“后台”进行耗时操做,好比大量运算、复制大文件、网络传输等。 使用Qt框架开发应用程序时,使用QThread类能够方便快捷地建立管理多线程。而多线程之间的通讯也可以使用Qt特有的“信号-槽”机制实现。 下面的说明以文件复制为例。主线程负责提供交互界面,显示复制进度等;子线程负责复制文件。最后附有能够执行的代码。c++

2. QThread使用方法1——重写run()函数

第一种使用方法是本身写一个类继承QThread,并重写其run()函数。 你们知道,C/C++程序都是从main()函数开始执行的。main()函数其实就是主进程的入口,main()函数退出了,则主进程退出,整个进程也就结束了。 而对于使用Qthread建立的进程而言,run()函数则是新线程的入口,run()函数退出,意味着线程的终止。复制文件的功能,就是在run()函数中执行的。 下面举个文件复制的例子。自定义一个类,继承自Qthreadgit

CopyFileThread: public QThread
{
    Q_OBJECT
public:
    CopyFileThread(QObject * parent = 0);

protected:
    void run(); // 新线程入口
// 省略掉一些内容
}

在对应的cpp文件中,定义run()github

void CopyFileThread::run()
{
    // 新线程入口
    // 初始化和操做放在这里
}

将这个类写好以后,在主线程的代码中生成一个CopyFileThread的实例,例如在mainwindow.cpp中写:网络

// mainwindow.h中
CopyFileThread * m_cpyThread;

// mainwindow.cpp中
m_cpyThread = new CopyFileThread;

在要开始复制的时候,好比按下“复制”按钮后,让这个线程开始执行:多线程

m_cpyThread->start();

注意,使用start()函数来启动子线程,而不是run()。start()会自动调用run()。 线程开始执行后,就进入run()函数,执行复制文件的操做。而此时,主线程的显示和操做都不受影响。 若是须要进行对复制过程当中可能发生的事件进行处理,例如界面显示复制进度、出错返回等等,应该从CopyFileThread中发出信号(signal),并事先链接到mainwindow的槽,由这些槽函数来处理事件。框架

3. QThread使用方法2——moveToThread()

若是不想每执行一种任务就自定义一个新线程,那么能够自定义用于完成任务的类,并让它们继承自QObject。例如,自定义一个FileCopier类,用于复制文件。函数

class FileCopier : public QObject
{
    Q_OBJECT
public:
    explicit FileCopier(QObject *parent = 0);

public slots:
    void startCopying();
    void cancelCopying();
}

注意这里咱们定义了两个槽函数,分别用于复制的开始和取消。 这个类自己的实例化是在主线程中进行的,例如:动画

// mainwindow.h中
private:
    FileCopier* m_copier;

// mainwindow.cpp中,初始化时
    m_copier = new FileCopier;

此时m_copier仍是属于主线程的。要将其移动到子线程处理,须要首先声明并实例化一个QThread:this

// mainwindow.h中
signals:
    void startCopyRsquested();
private:
    QThread * m_childThread; // m_copier将被移动到此线程执行

// mainwindow.cpp中,初始化时
    m_childThread = new QThread; // 子线程,自己不负责复制

而后使用moveToThread()将m_copier移动到新线程。注意moveToThread()是QObject的公有函数,所以用于复制文件的类FileCopier必须继承自QObject。移动以后启动子线程。此时复制尚未开始。spa

m_copier->moveToThread(m_childThread); // 将实例移动到新的线程,实现多线程运行
    m_childThread->start(); // 启动子线程

注意必定要记得启动子线程,不然线程没有运行,m_copier的功能也没法执行。 要开始复制,须要使用信号-槽机制,触发FileCopier的槽函数实现。所以要事先定义信号并链接:

// mainwindow.h中
signals:
    void startCopyRsquested();
// mainwindow.cpp中,初始化时
// 使用信号-槽机制,发出开始指令
    connect(this, SIGNAL(startCopyRsquested()), m_copier, SLOT(startCopying()));

当按下“复制”按钮后,发出信号。

emit startCopyRsquested(); // 发送信号

m_copier在另外一个线程接收到信号后,触发槽函数,开始复制文件。

4.常见问题

4.1. 子线程中能不能进行UI操做?

Qt中的UI操做,好比QMainWindow、QWidget之类的建立、操做,只能位于主线程! 这个限制意味着你不能在新的线程中使用QDialog、QMessageBox等。好比在新线程中复制文件出错,想弹出对话框警告?能够,可是必须将错误信息传到主线程,由主线程实现对话框警告。 所以通常思路是,主线程负责提供界面,子线程负责无UI的单一任务,经过“信号-槽”与主线程交互。

4.2. QThread中的哪些代码属于子线程?

QThread,以及继承QThread的类(如下统称QThread),他们的实例都属于新线程吗?答案是:不。 须要注意的是,QThread自己的实例是属于建立该实例的线程的。好比在主线程中建立一个QThread,那么这个QThread实例自己属于主线程。固然,QThread会开辟一个新线程(入口是run()),可是QThread自己并不属于这个新线程。也就是说,QThread自己的成员都不属于新线程,并且在QThread构造函数里经过new获得的实例,也不属于新线程。这一特性意味着,若是要实现多线程操做,那么你但愿属于新线程的实例、变量等,应该在run()中进行初始化、实例化等操做。本文给出的例子就是这样操做的。 若是你的多线程程序运行起来,会出现关于thread的报警,思考一下,各类变量、实例是否是放对了位置,是否是真的位于新的线程里。

4.3. 怎么查看是否是真的实现了多线程?

能够打印出当前线程。对于全部继承自QObject的类,例如QMainwindow、QThread,以及自定义的各类类,能够调用QObject::thread()查看当前线程,这个函数返回的是一个QThread的指针。例如用qDebug()打印: 在mainwindow.cpp的某个函数里、QThread的run()函数里、自定义类的某个函数里,写上:

qDebug() << "Current thread:" << thread();

对比不一样位置打印的指针,就能够知道它们是否是位于同一个线程了。

5.范例

范例实现了多线程复制文本文件。 提供的范例文件可用QtCreator编译运行。界面以下(不一样的操做系统略有不一样):

范例中实现了本文介绍的两种方法,同时也给出了单线程复制对比。打钩选择不一样的复制方法。能够发现,在使用多线程的时候,界面不会假死,第二根进度条的动画是持续的;而使用单线程复制的时候,“取消”按钮按不动,界面假死,并且第二根进度条的动画也中止了。 因为范例处理的文件很小,为了让复制过程持续较长时间以便使得现象明显,复制文件的时候,每复制一行加入了等待。

范例代码: https://github.com/Xia-Weiwen/CopyFile

相关文章
相关标签/搜索