Qt中的多线程与线程池浅析+实例

时间:2024-03-22 19:35:14

1. Qt中的多线程与线程池

今天学习了Qt中的多线程和线程池,特写这篇博客来记录一下

2. 多线程

2.1 线程类 QThread

Qt 中提供了一个线程类,通过这个类就可以创建子线程了,Qt 中一共提供了两种创建子线程的方式,先看一下这个类中提供的一些常用 API 函数:

2.1.1 常用函数

// QThread 类常用 API
// 构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;
// 判断子线程是不是在执行任务
bool QThread::isRunning() const; // Qt中的线程可以设置优先级
// 得到当前线程的优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
优先级:
QThread::IdlePriority --> 最低的优先级
QThread::LowestPriority
QThread::LowPriority
QThread::NormalPriority
QThread::HighPriority
QThread::HighestPriority
QThread::TimeCriticalPriority
QThread::InheritPriority --> 最高的优先级, 默认是这个 // 退出线程, 停止底层的事件循环
// 退出线程的工作函数
void QThread::exit(int returnCode = 0);
// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是
// 等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);

2.1.2 信号槽

// 和调用 exit() 效果是一样的
// 代用这个函数之后, 再调用 wait() 函数
[slot] void QThread::quit();
// 启动子线程
[slot] void QThread::start(Priority priority = InheritPriority);
// 线程退出, 可能是会马上终止线程, 一般情况下不使用这个函数
[slot] void QThread::terminate(); // 线程中执行的任务完成了, 发出该信号
// 任务函数中的处理逻辑执行完毕了
[signal] void QThread::finished();
// 开始工作之前发出这个信号, 一般不使用
[signal] void QThread::started();

2.1.3 静态函数

// 返回一个指向管理当前执行线程的QThread的指针
[static] QThread *QThread::currentThread();
// 返回可以在系统上运行的理想线程数 == 和当前电脑的 CPU 核心数相同
[static] int QThread::idealThreadCount();
// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs); // 单位: 毫秒
[static] void QThread::sleep(unsigned long secs); // 单位: 秒
[static] void QThread::usleep(unsigned long usecs); // 单位: 微秒

2.1.4 run()函数

// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();

run()函数非常重要,当线程执行的时候,就是去执行run()函数中的代码

2.2 使用方式一

  1. 需要创建一个线程类的子类,让其继承 QT 中的线程类 QThread
  2. 重写父类的 run () 方法,在该函数内部编写子线程要处理的具体的业务流程
  3. 在主线程中创建子线程对象,new 一个就可以了
  4. 启动子线程,调用 start () 方法

    当子线程别创建出来之后,父子线程之间的通信可以通过信号槽的方式,注意事项:
  • 在 Qt 中在子线程中不要操作程序中的窗口类型对象,不允许,如果操作了程序就挂了
  • 只有主线程才能操作程序中的窗口对象,默认的线程就是主线程,自己创建的就是子线程

2.3 实例

现在我们来完成一个功能,就是先随机生成很多随机数,然后通过冒泡排序,和快速排序的方法去执行,并且显示出来

  1. 首先画出一个窗口

    Qt中的多线程与线程池浅析+实例

    长这个样子

  2. 创建线程类 MyThread

说明:

  • Generate类是用来生成随机数的,其中有个槽方法recvNum,用来接受start信号传进来的参数,参数的值为生成的随机数的个数,run()方法为生成随机数的代码
  • BubbleSort和QuickSort类 都是用来排序的类,只是排序的方法不同,其中recvArray是为了接受传过来的随机数用来排序,finish信号是将排序好的数组传给主线程

头文件 MyThread.h



#ifndef MYTHREAD_H
#define MYTHREAD_H #include <QThread>
#include <QVector> class Generate : public QThread
{
Q_OBJECT
public:
explicit Generate(QObject *parent = nullptr); void recvNum(int num); protected:
void run() override; private:
int m_num; signals:
void sendArray(QVector<int> num); }; // 冒泡线程类
class BubbleSort : public QThread
{
Q_OBJECT
public:
explicit BubbleSort(QObject *parent = nullptr); void recvArray(QVector<int> list); protected:
void run() override; private:
QVector<int> m_list; signals:
void finish(QVector<int> num); }; // 快排线程类
class QuickSort : public QThread
{
Q_OBJECT
public:
explicit QuickSort(QObject *parent = nullptr); void recvArray(QVector<int> list); protected:
void run() override; private:
QVector<int> m_list;
void quickSort(QVector<int> &s,int l,int r); signals:
void finish(QVector<int> num); };
#endif // MYTHREAD_H

源文件mythread.cpp

说明:

  • 这是对上述一些函数的实现
#include "mythread.h"
#include <QDebug>
#include <QElapsedTimer>
Generate::Generate(QObject *parent) : QThread(parent)
{ } void Generate::recvNum(int num)
{
m_num = num;
} void Generate::run()
{
qDebug() << "生成随机数的线程地址: " << QThread::currentThread();
QVector<int> list;
QElapsedTimer time;
time.start();
for(int i=0;i<m_num;i++)
{
list.push_back(qrand()%10000);
}
int milsec = time.elapsed();
qDebug() << "生成" << m_num << "个随机数总共用时: " << milsec << "毫秒";
emit sendArray(list);
} BubbleSort::BubbleSort(QObject *parent) : QThread(parent)
{ } void BubbleSort::recvArray(QVector<int> list)
{
m_list = list;
} void BubbleSort::run()
{
qDebug() << "冒泡排序的线程地址: " << QThread::currentThread(); QElapsedTimer time;
time.start();
for(int i=0;i<m_list.size();i++){
for(int j=0;j<m_list.size()-i-1;j++){
if(m_list[j]>m_list[j+1]){
int temp = m_list[j];
m_list[j] = m_list[j+1];
m_list[j+1] = temp;
}
}
}
int milsec = time.elapsed();
qDebug() << "冒泡排序用时: " << milsec << "毫秒";
emit finish(m_list);
} QuickSort::QuickSort(QObject *parent) : QThread(parent)
{ } void QuickSort::recvArray(QVector<int> list)
{
m_list = list;
} void QuickSort::run()
{
qDebug() << "快速排序的线程地址: " << QThread::currentThread(); QElapsedTimer time;
time.start();
quickSort(m_list,0,m_list.size()-1);
int milsec = time.elapsed();
qDebug() << "快速排序用时: " << milsec << "毫秒";
emit finish(m_list);
} void QuickSort::quickSort(QVector<int> &s, int l, int r)
{
if (l< r)
{
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j]>= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i]< x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quickSort(s, l, i - 1); // 递归调用
quickSort(s, i + 1, r);
}
}
  1. 在主窗口类中实现相关功能

说明:

  • 头文件中定义了信号函数start(int num)用来发出信号,告诉要生成的随机数的数量

头文件MainWindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H #include <QMainWindow> QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE class MainWindow : public QMainWindow
{
Q_OBJECT public:
MainWindow(QWidget *parent = nullptr);
~MainWindow(); private:
Ui::MainWindow *ui; signals:
void starting(int num);
};
#endif // MAINWINDOW_H

源文件mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h" MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this); // 1. 创建子线程对象
Generate* gen = new Generate;
BubbleSort* bubble = new BubbleSort;
QuickSort* quick = new QuickSort; connect(this,&MainWindow::starting,gen,&Generate::recvNum);
// 2. 启动子线程
connect(ui->startBtn,&QPushButton::clicked,this,[=](){ emit starting(10000);
gen->start();
}); connect(gen,&Generate::sendArray,bubble,&BubbleSort::recvArray);
connect(gen,&Generate::sendArray,quick,&QuickSort::recvArray); // 3. 接受子线程发送的数据
connect(gen,&Generate::sendArray,this,[=](QVector<int> list)
{
bubble->start();
quick->start();
for(int i=0; i<list.size();++i){
ui->randList->addItem(QString::number(list.at(i)));
}
}); connect(bubble,&BubbleSort::finish,this,[=](QVector<int> list)
{
for(int i=0; i<list.size();++i){
ui->bubbleList->addItem(QString::number(list.at(i)));
}
}); connect(quick,&QuickSort::finish,this,[=](QVector<int> list)
{
for(int i=0; i<list.size();++i){
ui->quickList->addItem(QString::number(list.at(i)));
}
}); connect(this,&MainWindow::destroyed,this,[=](){
gen->quit();
gen->wait();
gen->deleteLater(); // delete gen; bubble->quit();
bubble->wait();
bubble->deleteLater(); quick->quit();
quick->wait();
quick->deleteLater(); }); } MainWindow::~MainWindow()
{
delete ui;
}

注意最后对线程的析构

 connect(this,&MainWindow::destroyed,this,[=](){
gen->quit();
gen->wait();
gen->deleteLater(); // delete gen; bubble->quit();
bubble->wait();
bubble->deleteLater(); quick->quit();
quick->wait();
quick->deleteLater();
});
  1. 运行结果

    Qt中的多线程与线程池浅析+实例

    Qt中的多线程与线程池浅析+实例

    可以发现,在对一万个随机数进行排序时,快速排序要比冒泡排序快很多

2.4 多线程使用方式二

Qt 提供的第二种线程的创建方式弥补了第一种方式的缺点,用起来更加灵活,但是这种方式写起来会相对复杂一些,其具体操作步骤如下:

  1. 创建一个新的类,让这个类从 QObject 派生
  2. 在这个类中添加一个公共的成员函数,函数体就是我们要子线程中执行的业务逻辑
  3. 在主线程中创建一个 QThread 对象,这就是子线程的对象
  4. 在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象)
  5. 将 MyWork 对象移动到创建的子线程对象中,需要调用 QObject 类提供的 moveToThread() 方法
  6. 启动子线程,调用 start(), 这时候线程启动了,但是移动到线程中的对象并没有工作
  7. 调用 MyWork 类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的

使用这种多线程方式,假设有多个不相关的业务流程需要被处理,那么就可以创建多个类似于 MyWork 的类,将业务流程放多类的公共成员函数中,然后将这个业务类的实例对象移动到对应的子线程中 moveToThread() 就可以了,这样可以让编写的程序更加灵活,可读性更强,更易于维护。

2.5 方式一与方式二区别

  • 方式一需要重载run()函数,run()函数不能带有参数,使得我们获取参数只能通过信号槽的方式去解决
  • 方式二更加灵活,将一个方法可以放到同一个线程中,也可以放到不同的线程中

3. 线程池

3.1 线程池原理

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

在各个编程语言的语种中都有线程池的概念,并且很多语言中直接提供了线程池,作为程序猿直接使用就可以了,下面给大家介绍一下线程池的实现原理:

线程池的组成主要分为 3 个部分,这三部分配合工作就可以得到一个完整的线程池:

  1. 任务队列,存储需要处理的任务,由工作的线程来处理这些任务

    1)通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除

    2)已处理的任务会被从任务队列中删除

    3)线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程

  2. 工作的线程(任务队列任务的消费者) ,N 个

    1) 线程池中维护了一定数量的工作线程,他们的作用是是不停的读任务队列,从里边取出任务并处理

    2) 工作的线程相当于是任务队列的消费者角色,

    3) 如果任务队列为空,工作的线程将会被阻塞 (使用条件变量 / 信号量阻塞)

    4) 如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作

  3. 管理者线程(不处理任务队列中的任务),1 个

    1) 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测

    2) 当任务过多的时候,可以适当的创建一些新的工作线程

    3) 当任务过少的时候,可以适当的销毁一些工作的线程

3.2 QRunnable

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

QRunnable 类 常用函数不多,主要是设置任务对象传给线程池后,是否需要自动析构。

// 在子类中必须要重写的函数, 里边是任务的处理流程
[pure virtual] void QRunnable::run(); // 参数设置为 true: 这个任务对象在线程池中的线程中处理完毕, 这个任务对象就会自动销毁
// 参数设置为 false: 这个任务对象在线程池中的线程中处理完毕, 对象需要程序猿手动销毁
void QRunnable::setAutoDelete(bool autoDelete);
// 获取当然任务对象的析构方式,返回true->自动析构, 返回false->手动析构
bool QRunnable::autoDelete() const;

3.3 QThreadPool

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

// 获取和设置线程中的最大线程个数
int maxThreadCount() const;
void setMaxThreadCount(int maxThreadCount); // 给线程池添加任务, 任务是一个 QRunnable 类型的对象
// 如果线程池中没有空闲的线程了, 任务会放到任务队列中, 等待线程处理
void QThreadPool::start(QRunnable * runnable, int priority = 0);
// 如果线程池中没有空闲的线程了, 直接返回值, 任务添加失败, 任务不会添加到任务队列中
bool QThreadPool::tryStart(QRunnable * runnable); // 线程池中被激活的线程的个数(正在工作的线程个数)
int QThreadPool::activeThreadCount() const; // 尝试性的将某一个任务从线程池的任务队列中删除, 如果任务已经开始执行就无法删除了
bool QThreadPool::tryTake(QRunnable *runnable);
// 将线程池中的任务队列里边没有开始处理的所有任务删除, 如果已经开始处理了就无法通过该函数删除了
void QThreadPool::clear(); // 在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象
static QThreadPool * QThreadPool::globalInstance();

一般情况下,我们不需要在 Qt 程序中创建线程池对象,直接使用 Qt 为每个应用程序提供的线程池全局对象即可。得到线程池对象之后,调用 start() 方法就可以将一个任务添加到线程池中,这个任务就可以被线程池内部的线程池处理掉了,使用线程池比自己创建线程的这种多种多线程方式更加简单和易于维护。

3.4 实例

我们将之前的程序改为线程池来实现,具体的方法就是创建线程池,然后将这些线程添加到线程中

发现:

Qt中的多线程与线程池浅析+实例

图中生成随机数的线程地址和冒泡排序的地址是相同的,这是为什么呢?

  • 因为当生成随机数的线程执行完之后,没有事情做了,这个时候它就会去处理下一个任务,因此地址相同
  • 而且放入线程池当中的任务,不需要自己去释放,而是线程池统一管理

4. 参考链接

https://subingwen.cn/qt/thread/