Qt 线程 QThread类详解

Qt 线程中QThread的使用

在进行桌面应用程序开发的时候, 假设应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,多个线程各司其职,不仅可以提高用户体验还可以提升程序的执行效率

在 qt 中使用了多线程,有些事项是需要额外注意的:

  • 默认的线程在Qt中称之为窗口线程,也叫主线程,负责窗口事件处理或者窗口控件数据的更新
  • 子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理
  • 主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制

1. 线程类 QThread

Qt 中提供了一个线程类,通过这个类就可以创建子线程了,Qt 中一共提供了两种创建子线程的方式,后边会依次介绍其使用方式。先来看一下这个类中提供的一些常用 API 函数:

// Qt中的线程可以设置优先级
// 得到当前线程的优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
优先级:QThread::IdlePriority         --> 最低的优先级QThread::LowestPriorityQThread::LowPriorityQThread::NormalPriorityQThread::HighPriorityQThread::HighestPriorityQThread::TimeCriticalPriority --> 最高的优先级QThread::InheritPriority      --> 子线程和其父线程的优先级相同, 默认是这个start(): 启动线程,使线程进入运行状态,调用线程的run()方法。
run(): 线程的执行函数,需要在该函数中编写线程所需执行的任务。
quit(): 终止线程的事件循环,在下一个事件处理周期结束时退出线程。
wait(): 阻塞当前线程,直到线程执行完成或超时。
finished(): 在线程执行完成时发出信号。
terminate(): 强制终止线程的执行,不推荐使用,可能导致资源泄漏和未定义行为。
isRunning(): 判断线程是否正在运行。
currentThreadId(): 返回当前线程的ID。
yieldCurrentThread(): 释放当前线程的时间片,允许其他线程执行。
msleep(): 让当前线程休眠指定的毫秒数。

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 时的一个常见误区。

  1. QThread 实例存在于实例化它的旧线程中:

    • 当您创建一个 QThread 对象时,该对象会存在于创建它的线程中,而不是在新创建的线程中。
    • 这意味着,您调用 QThread 对象的方法和槽时,实际上是在创建该对象的线程中执行的,而不是在新创建的工作线程中。
  2. 在新线程中调用槽:

    • 如果您希望在新创建的工作线程中执行某些操作,比如调用槽函数,就不能直接将这些槽函数定义在 QThread 子类中。
    • 因为 QThread 子类的方法和槽都会在创建该对象的线程中执行,而不是在工作线程中执行。
  3. 使用工作对象方法:

    • 为了在新创建的工作线程中执行操作,您需要定义一个独立的工作对象类,并将所有的工作逻辑封装在该类中。
    • 在工作线程中,您可以创建这个工作对象的实例,并在该实例上调用方法来执行工作。

使用 QThread 时需要注意区分线程的概念。QThread 对象本身存在于创建它的线程中,而不是在新创建的工作线程中。如果您希望在工作线程中执行操作,需要使用独立的工作对象,而不是直接在 QThread 子类中实现。这样可以确保在正确的线程中执行您的工作逻辑。 

简化:在这里先记住

1.new QThread 是在当前代码位置创建出来的

2.线程调用槽函数是有坑点的。

3.最好使用工作对象方法

线程和工作逻辑

  • 主线程和子线程执行的顺序不确定,偶尔主线程在前,偶尔子线程在前。
  • 只有run()函数运行在子线程中,run调用子函数则在子线程调用子函数。
class WorkerThread : public QThread
{Q_OBJECTpublic: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_OBJECTpublic: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_OBJECTpublic: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)

  1. 线程的创建方式

  2. ***线程与工作对象的关系:

    • 在第一种方式中,线程和工作逻辑是耦合在一起的,因为工作逻辑直接在 QThread 的子类中实现。
    • 在第二种方式中,线程和工作对象是解耦的,工作逻辑被封装在独立的 Worker 对象中。
  3. ***信号槽的执行位置:
    • move版本槽函数无论是线程的信号触发还是自定义信号触发,槽函数都在新线程里运行。
    • 重写版本信号槽是在主线程执行。
  4. 线程安全性:

    • 在第二种方式中,由于工作对象是在工作线程中运行的,因此不需要担心线程安全性问题。
    • 在第一种方式中,如果在 QThread 的子类中访问了共享资源,就需要特别注意线程安全性。
  5. 灵活性和可重用性:

    重写灵活性和可重用性低,move版本低耦合
  6. 错误处理:

    • 在第二种方式中,可以更容易地在工作对象中捕获和处理错误,因为工作逻辑被封装在了独立的对象中。
    • 在第一种方式中,错误处理可能更加困难,因为工作逻辑和线程紧耦合在一起。

总的来说,主要区别在于代码结构和设计模式,而不是线程的创建方式。无论采用哪种方式,都是在主线程中创建和启动了工作线程。由于4.8更新了movetothread版本,既然是更新,说明这种方法更好,建议大家在使用时采用movetothread版本

参考文献:

Qt 线程中QThread的使用_qt qthread-CSDN博客

QThread 类 | Qt 核心 5.15.17 --- QThread Class | Qt Core 5.15.17

一文搞定之Qt多线程(QThread、moveToThread)_qthread movetothread-CSDN博客

最后附上源代码链接
对您有帮助的话,帮忙点个star

36-qthread-qmutex-qsemaphore · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/43507.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

你所不知道的关于AI的27个冷知识——算法的作用

算法的作用 亲爱的朋友们,今天我们要探讨的是一个关于“魔法配方”的故事,也就是AI世界里的算法。这些算法就像是古老的魔法咒语,让AI能够在我们生活中的各个角落施展奇迹。让我们一起来揭开这些神奇算法的面纱,看看它们是如何在幕后默默工作的。 什么是算法? 首先,让…

昇思MindSpore学习笔记6-02计算机视觉--ResNet50迁移学习

摘要&#xff1a; 记录MindSpore AI框架使用ResNet50迁移学习方法对ImageNet狼狗图片分类的过程、步骤。包括环境准备、下载数据集、数据集加载、构建模型、固定特征训练、训练评估和模型预测等。 一、概念 迁移学习的方法 在大数据集上训练得到预训练模型 初始化网络权重参数…

【机器学习】特征选择:精炼数据,提升模型效能

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 特征选择&#xff1a;精炼数据&#xff0c;提升模型效能引言为何进行特征选择&a…

Android面试题汇总-框架技术

一、OkHttp OkHttp是一个流行的HTTP客户端库&#xff0c;用于发送和接收HTTP网络请求。以下是OkHttp的关键特性和工作原理的概述&#xff1a; 执行请求: execute() 方法: 同步执行HTTP请求&#xff0c;返回Response对象。enqueue() 方法: 异步执行HTTP请求&#xff0c;通过Ca…

STM32的独立看门狗详解

目录 1.独立看门狗是什么&#xff1f; 2.独立看门狗的作用 3.独立看门狗的实现原理 4.独立看门狗用到的寄存器 4.1 IWDG_KR &#xff08;关键字计时器&#xff09; 4.2 IWDG_PR&#xff08;预分频寄存器&#xff09; 4.3 IWDG_RLR&#xff08;重装载寄存器&#xff09…

云原生存储:使用MinIO与Spring整合

在现代云原生应用开发中&#xff0c;高效、可靠的存储解决方案是至关重要的。MinIO是一个高性能、分布式的对象存储系统&#xff0c;它与Amazon S3兼容&#xff0c;非常适合在Kubernetes等云原生环境中使用。本文将详细介绍如何在Spring Boot应用中整合MinIO&#xff0c;并提供…

接口调用的三种方式

例子&#xff1a; curl --location http://110.0.0.1:1024 \ --header Content-Type: application/json \ --data {"task_id": 1 }方式一&#xff1a;postman可视化图形调用 方式二&#xff1a;Vscode中powershell发送请求 #powershell (psh) Invoke-WebRequest -U…

熟悉Realsense和机械臂的控制库(如MoveIt!)的使用,以及基本的PID控制和其他控制算法

项目1&#xff1a;基础Realsense数据采集与处理 目标&#xff1a;了解Realsense摄像头的基本使用&#xff0c;数据采集和处理。 步骤&#xff1a; 安装并配置Realsense SDK。使用Realsense摄像头采集深度图像和RGB图像。实现基本的图像处理操作&#xff0c;如边缘检测、物体识…

温州海经区管委会主任、乐清市委书记徐建兵带队莅临麒麟信安调研

7月8日上午&#xff0c;温州海经区管委会主任、乐清市委书记徐建兵&#xff0c;乐清市委常委、副市长叶序锋&#xff0c;乐清市委办主任郑志坚一行莅临麒麟信安调研&#xff0c;乐清市投资促进服务中心及湖南省浙江总商会相关人员陪同参加。麒麟信安董事长杨涛、总裁刘文清热情…

超图Environment.initialization报空指针

如果库没倒错&#xff0c;许可证也在&#xff0c;一般是权限问题&#xff0c;要确保以下三个读写权限都在&#xff0c;制定版本要动态申请&#xff0c;官方Demo的代码里动态申请权限少一个。 /*** 需要申请的权限数组*/protected String[] needPermissions {Manifest.permiss…

Linux - VIM 全面教程

Linux - VIM 全面教程 前言 VIM 是一个强大的文本编辑器&#xff0c;被广泛用于 Linux 系统上。对于许多程序员和系统管理员来说&#xff0c;熟练掌握 VIM 是一项非常重要的技能。本教程将全面介绍 VIM 的基础知识和高级功能&#xff0c;帮助你更好地利用这一工具。 目录 安…

elasticsearch集群模式部署

系统版本&#xff1a;CentOS Linux release 7.9.2009 (Core) es版本&#xff1a; elasticsearch-7.6.2 本次搭建es集群为三个节点 添加启动用户 添加之前用户要是创建好了的(这里的es用户并不是绝对要求&#xff0c;你可以根据具体的需要命名创建) visudo 修改配置文件 sys…

【吊打面试官系列-MyBatis面试题】使用 MyBatis 的 mapper 接口调用时有哪些要求?

大家好&#xff0c;我是锋哥。今天分享关于 【使用 MyBatis 的 mapper 接口调用时有哪些要求&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 使用 MyBatis 的 mapper 接口调用时有哪些要求&#xff1f; 1、Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的…

数据结构(初阶1)

文章目录 一、复杂度概念 二、时间复杂度 2.1 大O的渐进表示法 2.2 时间复杂度计算示例 2.2.1. // 计算Func2的时间复杂度&#xff1f; 2.2.2.// 计算Func3的时间复杂度&#xff1f; 2.2.3.// 计算Func4的时间复杂度&#xff1f; 2.2.4.// 计算strchr的时间复杂度&#xff1f; …

Sharding-JDBC分库分表之SpringBoot分片策略

Sharding-JDBC系列 1、Sharding-JDBC分库分表的基本使用 2、Sharding-JDBC分库分表之SpringBoot分片策略 前言 前一篇以一个示例分享了Sharding-JDBC的基本使用。在进行分库分表时&#xff0c;可以设置分库分表的分片策略&#xff0c;在示例中&#xff0c;使用的是最简单的…

面对数据不一致性的解决方案:

polarDB是读写分离和计算存储分离的分布式数据库&#xff0c;并且副本的log replicate是基于Parallel-Raft协议来实现的。所以在瞬时进行写和读的操作时&#xff0c;是不可避免会存在数据一致性问题&#xff0c;导致这个数据一致性问题的原因不是事务&#xff0c;而是多副本日志…

springboot篮球馆管理系统-计算机毕业设计源码21945

目 录 摘要 1 绪论 1.1选题背景 1.2研究意义 1.3论文结构与章节安排 2 篮球馆管理系统系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统用例分析 2.4 …

数据结构--二叉树收尾

1.二叉树销毁 运用递归方法 分类&#xff1a; 根节点左子树右子树&#xff08;一般都是这个思路&#xff0c;不断进行递归即可&#xff09; 选择方法&#xff08;分析)&#xff1a; 前序&#xff1a;如果直接销毁根就无法找到左子树右子树 中序&#xff1a;也会导致丢失其…

【算法】(C语言):快速排序(递归)、归并排序(递归)、希尔排序

快速排序&#xff08;递归&#xff09; 左指针指向第一个数据&#xff0c;右指针指向最后一个数据。取第一个数据作为中间值。右指针指向的数据 循环与中间值比对&#xff0c;若大于中间值&#xff0c;右指针往左移动一位&#xff0c;若小于中间值&#xff0c;右指针停住。右…

红酒的奇幻之旅:从葡萄园到酒杯的魔法

在世界的某个角落&#xff0c;隐藏着一场关于红酒的奇幻之旅。这是一场从葡萄园到酒杯的魔法变幻&#xff0c;将大自然的馈赠与人类的智慧很好结合&#xff0c;最终呈现在我们眼前的&#xff0c;是一杯散发着迷人香气的雷盛红酒。 一、葡萄园的魔法启幕 当清晨的第一缕阳光洒落…