废话不多说,在 Qt5 这个版本中,使用线程的方法一般有三个:
- 使用子类继承
QThread
类,并重写run()
函数 - 使用
moveToThread()
函数 - 使用
QThreadPool
线程池
下面逐个来分析一下。
注意!这里使用的 Qt 版本具体为 Qt 5.14.2,构建套件为 MinGW 32/64,且下面的代码均以 Qt Widgets Application 为例。同时,为了方便,会在.pro
工程文件中加入CONFIG += console
,显示出控制台窗口。
子类继承父类
如前所说,这种方法的具体做法就是使用子类继承QThread
,再通过重写run()
函数的做法来实现。
先看下run()
函数的函数原型:1
[virtual protected] void QThread::run()
这个函数是QThread
类的纯虚函数,按照 Qt 帮助文档中的解释:
The starting point for the thread. After calling start(), the newly created thread calls this function. The default implementation simply calls exec().
You can reimplement this function to facilitate advanced thread management. Returning from this method will end the execution of the thread.
所以,这个函数是线程的开始,当在主线程中使用start()
函数后,run()
函数就被会调用。下面,来实现一下。
创建项目
在 Qt Creator 中创建一个 Qt Widgets Application 项目,可以得到下面的代码:1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
1 |
|
1 |
|
在上面三个文件中,其中 main.cpp 和 mainwindow.h 这两个文件不需要任何改动。
创建子类
接着,我们创建继承自QThread
的子类myThread
,可以得到下面的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class myThread : public QThread
{
Q_OBJECT
public:
explicit myThread(QObject *parent = nullptr);
protected:
virtual void run() override;
signals:
};
注意,在上面的代码中,我们保留了Q_OBJECT
宏,这使得myThread
类的对象可以使用 Qt 的信号与槽机制。1
2
3
4
5
6
7
8
9
10
11
12
13
myThread::myThread(QObject *parent) : QThread(parent)
{
}
void myThread::run()
{
qDebug() << QThread::currentThread();
// do something
}
这里,我们重写了run()
函数,让其输出当前执行的线程地址。
使用子类
接着,我们需要在 mainwindow.cpp 中使用这个子类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << QThread::currentThread();
myThread *t = new myThread(this);
t->start();
}
MainWindow::~MainWindow()
{
delete ui;
}
我们在MainWindow
的构造函数中,创建了myThread
对象指针t
,并构造了一个其指向的myThread
对象,且参数为this
指针(这意味在MainWindow
对象被释放时,t
所指对象也会被释放掉)。同时为了便于比较,先输出了当前线程(也就是主线程)的地址。
然后编译运行,得到的控制台窗口结果:1
2QThread(0x1777a40)
myThread(0x32695d8)
可以看到两个线程的地址是不一样的。
使用moveToThread()
创建项目
同样在 Qt Creator 中创建一个 Qt Widgets Application 项目,得到的三个文件,其中 main.cpp 这个文件不需要任何改动。
创建功能类
由于不需要再继承QThread
,我们可以直接创建我们需要实现的功能类,比如myClass
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class myClass : public QObject
{
Q_OBJECT
public:
explicit myClass(QObject *parent = nullptr);
void working();
signals:
};
1 |
|
上述代码中,我们创建了一个继承QObject
的子类myClass
,并给这个类新增了working()
函数,且这个函数会在控制台打印当前线程的地址。
使用功能类
接着,我们在 mainwindow.cpp 中使用这个功能类。由于我们没有重写run()
函数,所以我们需要告诉线程什么时候开始执行功能类中我们定义的功能函数。这里,我们使用信号与槽来实现,让主线程手动发出信号告诉子线程执行任务(实际情况下,就可以与其他事件绑定在一起了)。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
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
signals:
void startThread(); // 新增信号
private:
Ui::MainWindow *ui;
};
在 mainWindos.h 中新增信号,然后在 mainWindow.cpp 中发送信号: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
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << QThread::currentThread();
QThread *t = new QThread(this);
myClass *myclass = new myClass;
myclass->moveToThread(t);
connect(this, &MainWindow::startThread, myclass, &myClass::working);
//这里测试,不论是先发送信号后启动线程,还是先启动线程再发送信号,都是可以的
emit startThread();
t->start();
// emit startThread();
}
MainWindow::~MainWindow()
{
delete ui;
}
上述代码中,我们创建了一个线程对象,并将功能类对象,通过moveToThread()
函数交给线程来执行。
使用QThreadPool
如果要使用QThreadPool
,需要继承QRunnable
类,这与第一种方法是类似的,但也有不同之处。
创建项目
我们还是同样在 Qt Creator 中创建一个 Qt Widgets Application 项目,得到的三个文件,其中 main.cpp 和 mainwindow.h 这两个文件不需要任何改动。
创建子类
接着,我们创建继承自QObject
和QRunnable
的子类myClass
,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class myClass : public QObject, public QRunnable
{
Q_OBJECT
public:
explicit myClass(QObject *parent = nullptr);
virtual void run() override;
signals:
};
与第一种方法类似,这里我们也需要重写run()
函数来实现我们需要的功能,同时我们让myClass
也继承QObject
的原因是为了满足其他需要。1
2
3
4
5
6
7
8
9
10
11
12
13
14
myClass::myClass(QObject *parent) : QObject(parent), QRunnable()
{
setAutoDelete(true);
}
void myClass::run()
{
qDebug() << QThread::currentThread();
// do something
}
需要注意的是,上面的代码中,在myClass
的构造函数中,调用了setAutoDelete()
这个函数,作用是设置run()
函数结束后,线程池QThreadPool
自动删除线程。
那为什么要在构造函数中设置呢?
可以看下 Qt 文档对这个函数的解释:
Enables auto-deletion if autoDelete is true; otherwise auto-deletion is disabled.
If auto-deletion is enabled, QThreadPool will automatically delete this runnable after calling run(); otherwise, ownership remains with the application programmer.
Note that this flag must be set before calling QThreadPool::start(). Calling this function after QThreadPool::start() results in undefined behavior.
See also autoDelete() and QThreadPool.
所以,在构造函数中设置的好处就是,在每一次调用QThreadPool::start()
之前,这个函数就一定会提前被调用了。
使用子类
最后,我们还是在 mainwindow.cpp 中使用这个子类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << QThread::currentThread();
myClass *myclass = new myClass;
QThreadPool::globalInstance()->start(myclass);
}
MainWindow::~MainWindow()
{
delete ui;
}
得到控制台的输出结果:1
2QThread(0x997a40)
QThread(0x2fc0e30, name = "Thread (pooled)")
可以看到,不同的线程被调用了,并且提示使用了线程池。
总结
以上三种方式,主要就是 Qt 中使用多线程的方式。现在,回过头来看一下这些方式的特点。
首先是第一种,需要通过重写run()
函数来完成功能,如果需要执行的操作多又杂,很容易造成run()
函数臃肿;同时,如果需要执行的任务种类过多,多个继承的情况下,又很容易导致类爆炸。更关键的一点是,任务代码与线程管理的代码耦合在一起了,不利于后续的改动。
但好处也很明显,使用起来简单,非常直接,容易理解,而且可以完全控制线程的执行流程(如启动、终止、优先级设置等)。
然后是第二种,这种方法符合 Qt 的事件驱动模型,通过信号与槽机制,将任务触发和线程管理分离,也符合 Qt 的设计哲学。而且,同一个功能类,可以用于多个线程,一个线程也可以执行多个任务,相当灵活,也能免去重复创建和销毁线程的开销。
但缺点也是存在的,比如需要创建额外的功能类和线程对象,并通过信号与槽连接,代码量一定会大一点;而且通过信号与槽连接时,连接方式需要格外注意。
最后是第三种,与第一种类似,也需要重写run()
函数,那么同样会造成run()
函数臃肿;而且由于使用了线程池,就无法控制任务优先级了,不同的任务子线程之间也无法通信,除非任务执行完成。
同样它的优点就是使用起来简单且高效,线程池能自动管理线程的创建、复用和销毁。而且,线程管理与功能代码实际是分开的,耦合度较低。
简单整理一下:
特性 | 子类继承父类 | 使用moveToThread() | 使用QThreadPool |
---|---|---|---|
复杂度 | 简单 | 复杂 | 简单 |
灵活度 | 低 | 高 | 低 |
资源效耗 | 高 | 中 | 低 |
适用场景 | 简单独立任务 | 复杂交互任务 | 批量独立任务 |
补充
主要补充以下几点:
上述代码中,所有创建子线程、子类的过程都是在主线程中完成的,也就是子类,子线程的构造函数的执行。比如,在第三种方法中,主线程先调用
myClass
的构造函数,得到一个myClass
对象。在 Qt 网络编程中,如果使用第一种或第三种方法实现多线程网络通信,老版本的 Qt 可能会不支持向子线程传递
QTcpSocket
类对象。这时,需要再用子类继承QTcpServer
类,并重写incomingConnection()
这个虚函数,拿到类型为qintptr
的socketDescriptor
,然后传递给子线程,子线程在使用socketDescriptor
构造出QTcpSocket
类对象,从而完成网络通信。另外,尽管 Qt 后面的版本支持跨线程传递 socket 了,但是 Qt 仍然不推荐这样做。