多线程编程(二)
③线程池
Qt中线程池的使用 | 爱编程的大丙
1>线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
2>线程池原理
在各个编程语言的语种中都有线程池的概念,并且很多语言中直接提供了线程池,作为程序猿直接使用就可以了,下面介绍一下线程池的实现原理:
线程池的组成主要分为3个部分,这三部分配合工作就可以得到一个完整的线程池:
1. 任务队列,存储需要处理的任务,由工作的线程来处理这些任务
- 通过线程池提供的API函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
- 已处理的任务会被从任务队列中删除
- 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
2. 工作的线程(任务队列任务的消费者),N个
- 线程池中维护了一定数量的工作线程,他们的作用是是不停的读任务队列,从里边取出任务并处理
- 工作的线程相当于是任务队列的消费者角色,
- 如果任务队列为空,工作的线程将会被阻塞(使用条件变量/信号量阻塞)
- 如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作
3. 管理者线程(不处理任务队列中的任务),1个
- 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
- 当任务过多的时候可以适当的创建一些新的工作线程
- 当任务过少的时候可以适当的销毁一些工作的线程
3>线程池的使用QRunnable
在Qt中使用线程池需要先创建任务,添加到线程池中的每一个任务都需要是一个QRunnable
类型,因此在程序中需要创建子类继承QRunnable
这个类,然后重写run()
方法,在这个函数中编写要在线程池中执行的任务,并将这个子类对象传递给线程池,这样任务就可以被线程池中的某个工作的线程处理掉了。
QRunnable
类常用函数不多,主要是设置任务对象传给线程池后,是否需要自动析构。
| 在子类中必须要重写的函数, 里边是任务的处理流程 |
|
|
| 获取当前任务对象的析构方式,返回true->自动析构, 返回false->手动析构 |
创建一个要添加到线程池中的任务类,处理方式如下:
class MyWork : public QObject, public QRunnable
{
Q_OBJECT
public:
explicit MyWork(QObject *parent = nullptr)
{
// 任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
~MyWork();
void run() override{};
}
在上面的示例中MyWork类是一个多重继承,如果需要在这个任务中使用Qt的信号槽机制进行数据的传递就必须继承Qobject
这个类,如果不使用信号槽传递数据就可以不继承了,只继承QRunnable
即可。
class MyWork :public QRunnable
{
Q_OBJECT
public:
explicit MyWork()
{
// 任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
~MyWork();
void run() override{};
}
4>QThreadPool
Qt中的QThreadPool
类管理了一组QThreads
,里边还维护了一个任务队列。QThreadPool
管理和回收各个QThread
对象,以帮助减少使用线程的程序中的线程创建成本。每个Qt应用程序都有一个全局QThreadPool
对象,可以通过调用globalInstance()
来访问它。也可以单独创建一个QThreadPool
对象使用。
线程池常用的API函数如下:
| 获取和设置线程中的最大线程个数 |
| 给线程池添加任务, 任务是一个 QRunnable 类型的对象,如果线程池中没有空闲的线程了, 任务会放到任务队列中, 等待线程处理 |
| 如果线程池中没有空闲的线程了, 直接返回值, 任务添加失败, 任务不会添加到任务队列中 |
| 线程池中被激活的线程的个数(正在工作的线程个数) |
| 尝试性的将某一个任务从线程池的任务队列中删除, 如果任务已经开始执行就无法删除了 |
| 将线程池中的任务队列里边没有开始处理的所有任务删除, 如果已经开始处理了就无法通过该函数删除了 |
| 在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象 |
一般情况下,我们不需要在Qt程序中创建线程池对象,直接使用Qt为每个应用程序提供的线程池全局对象即可。得到线程池对象之后,调用start()
方法就可以将一个任务添加到线程池中,这个任务就可以被线程池内部的线程池处理掉了,使用线程池比自己创建线程的这种多种多线程方式更加简单和易于维护。
使用示例:
class MyWork :public QRunnable
{
Q_OBJECT
public:
explicit MyWork();
~MyWork();
void run() override;
}
MyWork::MyWork() : QRunnable()
{
// 任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
void MyWork::run()
{
// 业务处理代码
......
}
MyWork::MyWork() : QRunnable()
{
// 任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
void MyWork::run()
{
// 业务处理代码
......
}
示例:生成随机数,然后使用冒泡排序和快速排序对其进行处理
这个示例再用线程池实现一次
mythread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include<QVector>
#include<QRunnable>
//生成随机数,将构造类的名字直接改成Generate更明确一些
class Generate :public QObject, public QRunnable
{
Q_OBJECT
public:
explicit Generate(QObject *parent = nullptr);
//将主线程传递过来的数保存到m_num
void recvNum(int num);
void run() override;
signals:
void sendArray(QVector<int>);
private:
int m_num;
};
class BubbleSort :public QObject, public QRunnable
{
Q_OBJECT
public:
explicit BubbleSort(QObject *parent = nullptr);
//将主线程传递过来的数保存到m_num
void recvArray(QVector<int> list);
void run() override;
signals:
void finish(QVector<int>);
private:
QVector<int> m_list;
};
class QuickSort :public QObject, public QRunnable
{
Q_OBJECT
public:
explicit QuickSort(QObject *parent = nullptr);
//将主线程传递过来的数保存到m_num
void recvArray(QVector<int> list);
void run() override;
private:
void quickSort(QVector<int> &list,int l, int r);
signals:
void finish(QVector<int>);
private:
QVector<int> m_list;
};
#endif // MYTHREAD_H
mythread.cpp
#include "mythread.h"
#include<QElapsedTimer>
#include<QDebug>
#include<QThread>
Generate::Generate(QObject *parent) :QObject(parent),QRunnable()
{
//设置自动析构
setAutoDelete(true);
}
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() % 100000);
}
int milsec = time.elapsed();
qDebug() << "生成" << m_num << "随机数总用时:" << milsec << "毫秒";
//发送给主线程
emit sendArray(list);
}
BubbleSort::BubbleSort(QObject *parent):QObject(parent),QRunnable()
{
//设置自动析构
setAutoDelete(true);
}
void BubbleSort::recvArray(QVector<int> list)
{
m_list = list;
}
void BubbleSort::run()
{
qDebug() << "冒泡排序的线程的线程地址:" << QThread::currentThread();
//计时
QElapsedTimer time;
time.start();
//冒泡排序
int temp;
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])
{
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):QObject(parent),QRunnable()
{
//设置自动析构
setAutoDelete(true);
}
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;
int x = s[l];
while(i < j)
{
while(i < j && s[j] >= x)
{
j--;
}
if(i < j)
{
s[i++] = s[j];
}
while(i < j && s[i] < x)
{
i++;
}
if(i < j)
{
s[j--] = s[i];
}
}
s[i] = x;
quickSort(s,l,i-1);
quickSort(s,i+1,r);
}
}
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();
signals:
void starting(int num);
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<mythread.h>
#include<QThreadPool>
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->start,&QPushButton::clicked,this,[=]()
{
emit starting(10000);
QThreadPool::globalInstance()->start(gen);
});
//随机数子线程发送来的数据触发冒泡排序和快速排序接收数据
connect(gen,&Generate::sendArray,bubble,&BubbleSort::recvArray);
connect(gen,&Generate::sendArray,quick,&QuickSort::recvArray);
connect(gen,&Generate::sendArray,this,[=](QVector<int> list)
{
//启动两个任务
QThreadPool::globalInstance()->start(bubble);
QThreadPool::globalInstance()->start(quick);
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)));
}
});
}
MainWindow::~MainWindow()
{
delete ui;
}
main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qRegisterMetaType<QVector<int>>("QVector<int>");
MainWindow w;
w.show();
return a.exec();
}
④线程安全
实现线程互斥和同步常用的类有:
- 互斥锁:QMutex、QMutexLocker
- 条件变量:QWaitCondition
- 信号量:QSemaphore
- 读写锁:QReadLocker、QWriteLocker、QReadWriteLock
多个线程加锁的对象,得是同一个对象,不同对象,此时不会产生锁的互斥,也就无法把并发执行变为串行执行
1>互斥锁
互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在Qt中,互斥锁主要是通过QMutex
类来处理。
OMutex
特点:QMutex
是Qt框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。
用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex;
mutex.lock();//上锁
//访问共享资源
//……
mutex.unlock();//解锁
OMutexLocker
特点:OMutexLocker
是QMutex
的辅助类,使用RAII
(Resource Acquisition Is Initialization)方式对互斥锁进行上锁和解锁操作。
用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。
QMutex mutex;
{
QMutexLocker locker(&mutex);//在作用域内自动上锁
//访问共享资源
//...
}//在作用域结束时自动解锁
QMutex mutex;
创建了一个QMutex
对象。- 在大括号
{}
内,创建了QMutexLocker
的实例locker
,它接受mutex
的地址作为参数。这将锁定mutex
。 - 在大括号内,可以安全地访问共享资源,因为互斥量已经被锁定,防止了其他线程同时访问。
- 当离开大括号的作用域时,
locker
的实例被销毁,它的析构函数会自动调用mutex.unlock()
,从而释放互斥量。
QReadWriteLocker
,QReadLocker
,QWriteLocker
特点:
QReadWriteLock
是读写锁类,用于控制读和写的并发访问。QReadLocker
用于读操作上锁,允许多个线程同时读取共享资源。QWriteLocker
用于写操作上锁,只允许一个线程写入共享资源。
用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。
OReadWriteLock rwLock;
//在读操作中使用读锁
{
QReadLocker locker(&rwLock);//在作用域内自动上读锁
//读取共享资源
//……
}//在作用域结束时自动解读锁
//在写操作中使用写锁
{
QWriteLocker locker(&rwLock);//在作用域内自动上写锁
//修改共享资源
//……
}//在作用域结束时自动解写锁
2>条件变量
在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。
在Qt中,专门提供了QWaitCondition
类来解决像上述这样的问题。
特点:QWaitCondition
是Qt框架提供的条件变量类,用于线程之间的消息通信和同步。
用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。
3>信号量
有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
特点:QSemaphore是Qt框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
用途:限制并发线程数量,用于解决一些资源有限的问题。