Qt 线程中QThread的使用
在进行桌面应用程序开发的时候, 假设应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,多个线程各司其职,不仅可以提高用户体验还可以提升程序的执行效率。
在 qt 中使用了多线程,有些事项是需要额外注意的:
- 默认的线程在Qt中称之为窗口线程,也叫主线程,负责窗口事件处理或者窗口控件数据的更新
- 子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理
- 主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制
1. 线程类 QThread
Qt 中提供了一个线程类,通过这个类就可以创建子线程了,Qt 中一共提供了两种创建子线程的方式,后边会依次介绍其使用方式。先来看一下这个类中提供的一些常用 API 函数:
// 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 信号槽
// 和调用 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();
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); // 单位: 微秒
4 重写函数
// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();
2种线程使用方式
使用 QThread
时的一个常见误区。
-
QThread 实例存在于实例化它的旧线程中:
- 当您创建一个
QThread
对象时,该对象会存在于创建它的线程中,而不是在新创建的线程中。 - 这意味着,您调用
QThread
对象的方法和槽时,实际上是在创建该对象的线程中执行的,而不是在新创建的工作线程中。
- 当您创建一个
-
在新线程中调用槽:
- 如果您希望在新创建的工作线程中执行某些操作,比如调用槽函数,就不能直接将这些槽函数定义在
QThread
子类中。 - 因为
QThread
子类的方法和槽都会在创建该对象的线程中执行,而不是在工作线程中执行。
- 如果您希望在新创建的工作线程中执行某些操作,比如调用槽函数,就不能直接将这些槽函数定义在
-
使用工作对象方法:
- 为了在新创建的工作线程中执行操作,您需要定义一个独立的工作对象类,并将所有的工作逻辑封装在该类中。
- 在工作线程中,您可以创建这个工作对象的实例,并在该实例上调用方法来执行工作。
使用 QThread
时需要注意区分线程的概念。QThread
对象本身存在于创建它的线程中,而不是在新创建的工作线程中。如果您希望在工作线程中执行操作,需要使用独立的工作对象,而不是直接在 QThread
子类中实现。这样可以确保在正确的线程中执行您的工作逻辑。
线程和工作逻辑
class WorkerThread : public QThread
{
Q_OBJECT
public:
void run() override
{
// 在新线程中执行耗时任务
for (int i = 0; i < 5; ++i)
{
qDebug() << "Worker thread doing work..." << i;
sleep(1);
}
emit finished();
}
signals:
void finished();
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 创建并启动工作线程
WorkerThread* workerThread = new WorkerThread;
QObject::connect(workerThread, &WorkerThread::finished, workerThread, &WorkerThread::deleteLater);
workerThread->start();
// 等待任务完成
workerThread->wait();
// 停止并退出事件循环
a.exit();
return a.exec();
}
这个例子演示了如何在一个全新的线程中执行耗时的工作任务。通过继承 QThread
并重写 run()
方法,我们可以在新线程中运行自定义的工作逻辑。这种方式与下列的例子有所不同,下列的例子是在工作对象中执行任务,而不是在 QThread
子类中。
线程和工作对象
#include <QDebug>
#include <QThread>
class Worker : public QObject
{
Q_OBJECT
public:
Worker()
{
}
public slots:
void doWork()
{
// 执行耗时的计算任务
for (int i = 0; i < 5; ++i)
{
qDebug() << "Worker thread doing work..." << i;
QThread::sleep(1);
}
// 计算完成后,发射 finished 信号
emit finished();
}
signals:
void finished();
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 创建工作线程
QThread* workerThread = new QThread;
// 创建工作对象实例,并将其移动到工作线程中
Worker* worker = new Worker;
worker->moveToThread(workerThread);//将其移动到工作线程中。这确保了 Worker 对象的所有操作都在工作线程中进行
// 连接信号和槽
QObject::connect(workerThread, &QThread::started, worker, &Worker::doWork);
QObject::connect(worker, &Worker::finished, workerThread, &QThread::quit);
//即使 main() 函数执行完毕,主线程退出,QThread 对象也不会立即被释放,而是会等待工作线程完成后再被删除。
QObject::connect(worker, &Worker::finished, worker, &Worker::deleteLater);
QObject::connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater);
// 启动工作线程
workerThread->start();
return a.exec();
}
在这个例子中,QThread 对象代表了实际的工作线程,而 Worker 对象包含了实际的工作逻辑。这样,我们就可以在工作线程中执行耗时的计算任务,而不会阻塞主线程。当任务完成时,Worker 对象会发射 finished 信号,从而触发工作线程退出和对象清理。
代码运行示例
重写线程类:work.h
#ifndef WORK_H
#define WORK_H
#include<QDebug>
#include<QThread>
#include<QMutex>
#include<QSemaphore>
class work : public QThread
{
Q_OBJECT
public:
work();
virtual ~work(); // 添加虚拟析构函数
public:
void run() override;
void stopAndWait() { //完美退出
m_running = false;
wait(); //等待任务完成
exit(); //停止并退出事件循环
qDebug() << "stopAndWait";
deleteLater(); //安全地删除 QObject 及其子类对象 异步地删除对象,而不是立即删除 注意:该函数是线程安全,多次调用该函数是安全的;
}
private:
bool m_running; //完美退出
static uint16_t index;
};
#endif // WORK_H
#include "work.h"
uint16_t work::index = 1;
work::work()
{
// 构造函数实现
m_running=true;
}
work::~work()
{
// 虚拟析构函数实现
}
void work::run() {
// 执行耗时工作
while (m_running) {
qDebug() << "Worker thread doing run..." << this->index++;
sleep(1);
}
qDebug() << "Worker thread exiting.";
}
工作对象类
workthread.h
#ifndef WORKTHREAD2_H
#define WORKTHREAD2_H
#include<QDebug>
#include<QThread>
class workthread2 : public QObject
{
Q_OBJECT
public:
workthread2()=default;
~workthread2()=default;
public:
enum class ThreadState { Idle, Running, Finished };
Q_ENUM(ThreadState)
signals:
void finished();
public slots:
void doWork()
{
// 执行耗时的计算任务
for (int i = 0; i < 5; ++i)
{
qDebug() << "Worker thread doing work..." << i;
QThread::sleep(1);
}
// 计算完成后,发射 finished 信号
emit finished();
}
};
#endif // WORKTHREAD2_H
main函数
#include <QCoreApplication>
#include"work.h"
#include<workthread2.h>
#include<QTimer>
void qtThread_text(){
//第一种 继承重写run方法
QVector<work*> workerThreads;
// 创建并启动工作线程
for(int i=0;i<3;i++)
{
workerThreads.append(new work());
}
for(int i=0;i<3;i++)
{
workerThreads.at(i)->start();
}
// 等待一段时间后,通知线程退出
QThread::sleep(1);
for(work* data :workerThreads){
data->stopAndWait();
//data->deleteLater();
}
// for(int i=0;i<3;i++)
// {
// workerthread[i]->wait();
// workerthread[i]->terminate(); //发送线程终止信号
// }
//第2种 使用对象模式 注意此模式是在当前线程运行
QThread* newthread= new QThread;
QThread* newthread2= new QThread;
// 启动工作线程 一个对象只能隶属于一个线程
workthread2* work2 = new workthread2;
workthread2* work2_2 = new workthread2;
work2->moveToThread(newthread);
work2_2->moveToThread(newthread2);
// 启动工作线程
newthread->start();
newthread2->start();
// 连接信号和槽
QObject::connect(newthread, &QThread::started, work2, &workthread2::doWork);
QObject::connect(work2, &workthread2::finished, newthread, &QThread::quit);
QObject::connect(work2, &workthread2::finished, work2, &workthread2::deleteLater);
QObject::connect(newthread, &QThread::finished, newthread, &QThread::deleteLater);
QObject::connect(newthread2, &QThread::started, work2_2, &workthread2::doWork);
QObject::connect(work2_2, &workthread2::finished, newthread2, &QThread::quit);
QObject::connect(work2_2, &workthread2::finished, work2_2, &workthread2::deleteLater);
QObject::connect(newthread2, &QThread::finished, newthread2, &QThread::deleteLater);
newthread->start();
newthread2->start();
/*
newthread->wait();
newthread->exit();
当 newthread->wait() 被调用时,它会阻塞当前线程(即主线程)直到 newthread 线程退出。然后 newthread->exit() 被调用,停止并退出了事件循环。
qDebug() << "a.exec()"; 这行代码就不会被执行。
*/
// 等待线程完成并退出事件循环
newthread->wait();
newthread2->wait();
newthread->exit();
newthread2->exit();
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qtThread_text();
// 手动调用 a.exec()
qDebug() << "a.exec()";
return a.exec(); //开启Qt 的事件循环
}
运行结果
总结
上述提到的两种使用 QThread
的方式有以下几点主要区别: (第一种重写 第二种movetoThread)
-
线程的创建方式:
- 第一种方式是继承
QThread
类,并在run()
方法中编写线程的工作逻辑。 - 第二种方式是创建一个
QThread
对象,并使用moveToThread()
将一个独立的工作对象移动到该线程中。
- 第一种方式是继承
-
***线程与工作对象的关系:
- 在第一种方式中,线程和工作逻辑是耦合在一起的,因为工作逻辑直接在
QThread
的子类中实现。 - 在第二种方式中,线程和工作对象是解耦的,工作逻辑被封装在独立的
Worker
对象中。
- 在第一种方式中,线程和工作逻辑是耦合在一起的,因为工作逻辑直接在
-
灵活性和可重用性:
- 第二种方式更加灵活和可重用。因为工作对象可以独立于线程而存在,可以在不同的线程中复用。
- 第一种方式的
QThread
子类更难复用,因为工作逻辑与线程紧耦合。
-
线程安全性:
- 在第二种方式中,由于工作对象是在工作线程中运行的,因此不需要担心线程安全性问题。
- 在第一种方式中,如果在
QThread
的子类中访问了共享资源,就需要特别注意线程安全性。
-
错误处理:
- 在第二种方式中,可以更容易地在工作对象中捕获和处理错误,因为工作逻辑被封装在了独立的对象中。
- 在第一种方式中,错误处理可能更加困难,因为工作逻辑和线程紧耦合在一起。
- ***信号槽的执行位置:
方式 1 中,由于 WorkerThread 对象本身就在新线程中,所以其信号槽都在新线程中执行。
方式 2 中,Worker 对象的槽函数 doWork() 是在新线程中执行的,而 Worker 对象本身以及其他信号槽仍然在创建 Worker 对象的主线程中执行。
总的来说,主要区别在于代码结构和设计模式,而不是线程的创建方式。无论采用哪种方式,都是在主线程中创建和启动了工作线程。
参考文献:
Qt 线程中QThread的使用_qt qthread-CSDN博客
QThread 类 | Qt 核心 5.15.17 --- QThread Class | Qt Core 5.15.17
最后附上源代码链接
对您有帮助的话,帮忙点个star
36-qthread-qmutex-qsemaphore · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)