线程
在qt中使用了多线程,有些事项是需要额外注意的:
默认的线程在Qt中称之为窗口线程,也叫主线程,负责窗口事件处理或者窗口控件数据的更新
子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理
主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制
为什么要使用多线程
提高用户界面的响应性
Qt 应用程序通常有一个主线程(也称为 GUI 线程),负责处理用户界面事件和绘制窗口。如果在这个线程中执行耗时的操作(如从网络下载数据、读取大文件或进行复杂的计算),会导致界面冻结,用户体验不佳。
通过将这些耗时操作移到后台线程中执行,可以确保主线程保持响应,从而提供流畅的用户体验。并行处理以提高性能
现代计算机通常具有多个 CPU 核心,能够并行执行多个任务。通过使用多线程,可以充分利用这些核心来加速计算密集型任务。
例如,在图像处理、视频编码、科学计算等领域,多线程可以显著提高性能。
3. 处理异步操作
许多操作(如网络请求、数据库查询、文件 I/O 等)是异步的,并且需要一定的时间才能完成。通过使用多线程,可以在等待这些操作完成的同时继续处理其他任务,而不会阻塞主线程。
Qt 提供了多种方式来处理异步操作,包括 QThread
、QThreadPool
和 QtConcurrent
。
QThread
方法一: 继承 QThread
类并重写 run()
方法。
Qt中提供的多线程的第一种使用方式的特点是: 简单。操作步骤如下:
- 需要创建一个线程类的子类,让其继承QT中的线程类 QThread,比如:
class MyThread:public QThread
{
......
}
- 重写父类的 run() 方法,在该函数内部编写子线程要处理的具体的业务流程
class MyThread:public QThread
{
......
protected:
void run()
{
........
}
}
- 在主线程中创建子线程对象,new 一个就可以了
MyThread * subThread = new MyThread;
- 启动子线程, 调用 start() 方法
subThread->start();
不能在类的外部调用run() 方法启动子线程,在外部调用start()相当于让run()开始运行
当子线程别创建出来之后,父子线程之间的通信可以通过信号槽的方式,注意事项:
在Qt中在子线程中不要操作程序中的窗口类型对象, 不允许, 如果操作了程序就挂了
只有主线程才能操作程序中的窗口对象, 默认的线程就是主线程, 自己创建的就是子线程
这种在程序中添加子线程的方式是非常简单的,但是也有弊端,假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到run()函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护。
mythread.h
#include <QThread>
class MyThread : public QThread
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = nullptr);
protected:
void run();
signals:
// 自定义信号, 传递数据
void curNumber(int num);
public slots:
};
mythread.cpp
#include "mythread.h"
#include <QDebug>
MyThread::MyThread(QObject *parent) : QThread(parent)
{
}
void MyThread::run()
{
qDebug() << "当前线程对象的地址: " << QThread::currentThread();
int num = 0;
while(1)
{
emit curNumber(num++);
if(num == 10000000)
{
break;
}
QThread::usleep(1);
}
qDebug() << "run() 执行完毕, 子线程退出...";
}
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "主线程对象地址: " << QThread::currentThread();
// 创建子线程
MyThread* subThread = new MyThread;
connect(subThread, &MyThread::curNumber, this, [=](int num)
{
ui->label->setNum(num);
});
connect(ui->startBtn, &QPushButton::clicked, this, [=]()
{
// 启动子线程
subThread->start();
});
}
MainWindow::~MainWindow()
{
delete ui;
}
方法二: 使用 moveToThread()
方法将工作对象移动到新的线程中。
1 操作步骤
Qt提供的第二种线程的创建方式弥补了第一种方式的缺点,用起来更加灵活,但是这种方式写起来会相对复杂一些,其具体操作步骤如下:
- 创建一个新的类,让这个类从QObject派生
class MyWork:public QObject
{
.......
}
- 在这个类中添加一个公共的成员函数,函数体就是我们要子线程中执行的业务逻辑
class MyWork:public QObject
{
public:
.......
// 函数名自己指定, 叫什么都可以, 参数可以根据实际需求添加
void working();
}
- 在主线程中创建一个QThread对象, 这就是子线程的对象
QThread* sub = new QThread;
- 在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象)
MyWork* work = new MyWork(this); // error
MyWork* work = new MyWork; // ok
- 将MyWork对象移动到创建的子线程对象中, 需要调用QObject类提供的moveToThread()方法
// void QObject::moveToThread(QThread *targetThread);
// 如果给work指定了父对象, 这个函数调用就失败了
// 提示: QObject::moveToThread: Cannot move objects with a parent
work->moveToThread(sub); // 移动到子线程中工作
- 启动子线程,调用 start(), 这时候线程启动了, 但是移动到线程中的对象并没有工作
- 调用MyWork类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的
- 继承
QThread
:适合简单的、单一的任务场景,但不适合复杂的任务和状态管理。 - 使用
moveToThread()
:适合复杂的任务和状态管理,能够更好地遵循面向对象设计原则,职责分离更加清晰。
mywork.h
#ifndef MYWORK_H
#define MYWORK_H
#include <QObject>
class MyWork : public QObject
{
Q_OBJECT
public:
explicit MyWork(QObject *parent = nullptr);
// 工作函数
void working();
signals:
void curNumber(int num);
public slots:
};
#endif // MYWORK_H
mywork.cpp
#include "mywork.h"
#include <QDebug>
#include <QThread>
MyWork::MyWork(QObject *parent) : QObject(parent)
{
}
void MyWork::working()
{
qDebug() << "当前线程对象的地址: " << QThread::currentThread();
int num = 0;
while(1)
{
emit curNumber(num++);
if(num == 10000000)
{
break;
}
QThread::usleep(1);
}
qDebug() << "run() 执行完毕, 子线程退出...";
}
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>
#include "mywork.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "主线程对象的地址: " << QThread::currentThread();
// 创建线程对象
QThread* sub = new QThread;
// 创建工作的类对象
// 千万不要指定给创建的对象指定父对象
// 如果指定了: QObject::moveToThread: Cannot move objects with a parent
MyWork* work = new MyWork;
// 将工作的类对象移动到创建的子线程对象中
work->moveToThread(sub);
// 启动线程
sub->start();
// 让工作的对象开始工作, 点击开始按钮, 开始工作
connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);
// 显示数据
connect(work, &MyWork::curNumber, this, [=](int num)
{
ui->label->setNum(num);
});
}
MainWindow::~MainWindow()
{
delete ui;
}
线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢
QRunnable
QRunnable
是一个抽象类,用于表示可以在 QThreadPool
中执行的任务。你需要继承 QRunnable
并实现其纯虚函数 run()
来定义任务的具体逻辑。
// 在子类中必须要重写的函数, 里边是任务的处理流程
[pure virtual] void QRunnable::run();
// 参数设置为 true: 这个任务对象在线程池中的线程中处理完毕, 这个任务对象就会自动销毁
// 参数设置为 false: 这个任务对象在线程池中的线程中处理完毕, 对象需要程序猿手动销毁
void QRunnable::setAutoDelete(bool autoDelete);
QThreadPool
QThreadPool
管理一组工作线程,并允许将 QRunnable
任务分配给这些线程。每个 Qt 应用程序都有一个全局的线程池实例,可以通过 QThreadPool::globalInstance()
获取。
// 在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象
static QThreadPool * QThreadPool::globalInstance();
作者: 苏丙榅
链接: Qt中多线程的使用 | 爱编程的大丙
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。