使用Qt编写程序时,为了避免耗时操作导致界面“假死”以及并行计算提高程序运行速度,我们需要使用多线程,下面记录四种多线程使用的方法。

1 QThread

第一种方法是使用 QThread 类,具体使用步骤如下:

  1. 需要创建一个线程类的子类,让其继承 QT 中的线程类 QThread,比如:
1
2
3
4
class MyThread:public QThread
{
......
}
  1. 重写父类的 run () 方法,在该函数内部编写子线程要处理的具体的业务流程,run 对于线程的作用相当于 main 函数对于应用程序。它是线程的入口,run 的开始和结束意味着线程的开始和结束。
1
2
3
4
5
6
7
8
9
10
class MyThread:public QThread
{
......
protected:
//run()函数是一个虚函数,让创建的子线程执行某个任务,需要写一个子类让其继承 QThread,并且在子类中重写父类的 run() 方法,函数体就是对应的任务处理流程
void run()
{
........
}
}
  1. 在主线程中创建子线程对象,new 一个就可以了
1
MyThread * subThread = new MyThread;
  1. 启动子线程,调用 start () 方法
1
2
//start()函数启动子线程,run()函数就会被调用了
subThread->start();

当子线程别创建出来之后,父子线程之间的通信可以通过信号槽的方式,注意事项:

  • 在 Qt 中在子线程中不要操作程序中的窗口类型对象,不允许,如果操作了程序就挂了
  • 只有主线程才能操作程序中的窗口对象,默认的线程就是主线程,自己创建的就是子线程。

这种在程序中添加子线程的方式是非常简单的,但是也有弊端,假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到 run() 函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护。

2 moveToThread

第二种线程的创建方式弥补了第一种方式的缺点,用起来更加灵活,使用到的是moveToThread() 方法。

  1. 创建一个新的类,让这个类从 QObject 派生
1
2
3
4
class MyWork:public QObject
{
.......
}
  1. 在这个类中添加一个公共的成员函数,函数体就是我们要子线程中执行的业务逻辑
1
2
3
4
5
6
7
class MyWork:public QObject
{
public:
.......
// 函数名自己指定, 叫什么都可以, 参数可以根据实际需求添加
void working();
}
  1. 在主线程中创建一个 QThread 对象,这就是子线程的对象
1
QThread* sub = new QThread;
  1. 在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象)
1
2
MyWork* work = new MyWork(this);    // error
MyWork* work = new MyWork; // ok
  1. 将 MyWork 对象移动到创建的子线程对象中,需要调用 QObject 类提供的 moveToThread() 方法
1
2
3
4
// void QObject::moveToThread(QThread *targetThread);
// 如果给work指定了父对象, 这个函数调用就失败了
// 提示: QObject::moveToThread: Cannot move objects with a parent
work->moveToThread(sub); // 移动到子线程中工作
  1. 启动子线程,调用 start(), 这时候线程启动了,但是移动到线程中的对象并没有工作
  2. 调用 MyWork 类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的(包括这个对象的槽函数也会运行在子线程

3 线程池 QRunnable + QThreadPool

线程池的作用是使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务,避免频繁创建线程降低效率。

Qt 中的 QThreadPool 类管理了一组 QThreads, 里边还维护了一个任务队列。QThreadPool 管理和回收各个 QThread 对象,以帮助减少使用线程的程序中的线程创建成本。每个Qt应用程序都有一个全局 QThreadPool 对象,可以通过调用 globalInstance() 来访问它。也可以单独创建一个 QThreadPool 对象使用。

在 Qt 中使用线程池需要先创建任务,添加到线程池中的每一个任务都需要是一个 QRunnable 类型,因此在程序中需要创建子类继承 QRunnable 这个类,然后重写 run() 方法,在这个函数中编写要在线程池中执行的任务,并将这个子类对象传递给线程池,这样任务就可以被线程池中的某个工作的线程处理掉了。

  1. 创建一个线程子类,让其继承 QRunnableQObject(继承 QObject 可以使用信号和槽)。
1
2
3
4
class MyWork :public QRunnable, public QObject
{
.......
}
  1. 重写父类的run()方法,在该函数内部编写子线程要处理的具体的业务流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyWork :public QRunnable, public QObject
{
Q_OBJECT
public:
explicit MyWork();
~MyWork();

void run() override;
}

MyWork::MyWork()
{
// 任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
void MyWork::run()
{
// 业务处理代码
......
}
  1. 在主线程中初始化线程池
1
2
// 线程池初始化,设置最大线程池数
QThreadPool::globalInstance()->setMaxThreadCount(4);
  1. 在主线程实例化子线程对象,并将其放入线程池
1
2
MyWork* task = new MyWork;
QThreadPool::globalInstance()->start(task);

4 QtConcurrent

QtConcurrent::run能够方便快捷的将任务丢到线程池的子线程中去执行**,无需继承任何类,也不需要重写函数**,使用非常简单,但是执行完毕后没有任何信号,目前我想到的还是需要在子线程函数中添加信号,完成后触发信号表示子线程函数运行完毕。

  1. 新建一个子线程类(也可以不建),将需要处理的程序放到一个公共函数中,并设置相应信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct myData //测试返回自定义数据类型
{
int test = 2;
};

class MyConcurrent : public QObject
{
Q_OBJECT
public:
myData work();

signals:
void sig_finished();
};
1
2
3
4
5
6
7
8
9
myData MyConcurrent::work()
{
qDebug() << "Concurrent子线程对象的地址: " << QThread::currentThread();
......
emit sig_finished();
myData m;
m.test = 1;
return m;
}
  1. 在主线程中实例化子线程类对象,将子线程函数完成的信号进行信号和槽连接。
1
2
3
MyConcurrent* mythread3 = new MyConcurrent;
connect(mythread3, SIGNAL(sig_finished()), this, SLOT(OnSigFinished()));

  1. 将其工作函数传入QtConcurrent::run()函数中,并使用QFuture接收函数返回值。
1
2
3
4
5
6
7
8
9
//在新的线程中调用普通函数 
template <typename T> QFuture<T> QtConcurrent::run(Function function, ...)
//使用线程池中的线程调用普通函数
template <typename T> QFuture<T> QtConcurrent::run(QThreadPool *pool, Function function, ...)
//在新的线程里调用成员函数 有多个重载实现不同的参数个数
template <typename T> QFuture<T> QtConcurrent::run(className *obejct, Function function, ...)

QFuture<myData> future = QtConcurrent::run(mythread3, &MyConcurrent::work);
//future.waitForFinished();//如果使用等待则会阻塞主线程,造成界面卡死
  1. 在主线程对应的槽函数中接收子线程计算的结果。
1
2
3
4
5
6
7
void MultiThread::OnSigFinished()
{
if (future.isFinished())
{
myData&& m = future.result();
}
}

5 比较

**需要线程的生命周期 ** 开发场景 解决方案
单次调用 在其他的线程中运行一个方法,当方法运行结束后退出线程。 (1)编写一个函数,然后利用 QtConcurrent::run()运行它;(2)从 QRunnable 派生一个类,并利用全局线程池QThreadPool::globalInstance()->start()来运行它。(3) 从QThread派生一个类, 重载QThread::run() 方法并使用QThread::start()来运行它。
单次调用 一个耗时的操作必须放到另一个线程中运行。在这期间,状态信息必须发送到GUI线程中。 使用 QThread,,重载run方法并根据情况发送信号。.使用queued信号/槽连接来连接信号与GUI线程的槽。
常驻 有一对象位于另一个线程中,将让其根据不同的请求执行不同的操作。这意味与工作者线程之间的通信是必须的。 从QObject 派生一个类并实现必要的槽和信号,将对象移到一个具有事件循环的线程中,并通过queued信号/槽连接与对象进行通信。

6 主要参考

大丙多线程

QtConcurrent::run()+QThreadPool

QT多线程开发

QT多线程之QtConcurrent::run()

7 实验代码

我的github