Qt之QThread(深入理解)

article/2025/10/16 9:10:11

简述

为了让程序尽快响应用户操作,在开发应用程序时经常会使用到线程。对于耗时操作如果不使用线程,UI界面将会长时间处于停滞状态,这种情况是用户非常不愿意看到的,我们可以用线程来解决这个问题。

前面,已经介绍了QThread常用的两种方式:

  • Worker-Object
  • 子类化QThread

下面,我们来看看子类化QThread在日常中的应用。

 

  • 简述
  • 子类化QThread
  • 线程休眠
  • 在主线程中更新UI
  • 优雅地结束线程
  • 更多参考

大多数情况下,多线程耗时操作会与UI进行交互,比如:显示进度、加载等待。。。让用户明确知道目前的状态,并对结果有一个直观的预期,甚至有趣巧妙的设计,能让用户爱上等待,把等待看成一件很美好的事。

子类化QThread

下面,是一个使用多线程操作UI界面的示例 - 更新进度条。与此同时,分享在此过程中有可能遇到的问题及解决方法。

这里写图片描述

定义一个WorkerThread类,让其继承自QThread,并重写run()函数,每隔50毫秒更新当前值,然后发射resultReady()信号(用于更新进度条)。

#include <QThread>class WorkerThread : public QThread{Q_OBJECTpublic:explicit WorkerThread(QObject *parent = 0): QThread(parent){qDebug() << "Worker Thread : " << QThread::currentThreadId();}protected:virtual void run() Q_DECL_OVERRIDE {qDebug() << "Worker Run Thread : " << QThread::currentThreadId();int nValue = 0;while (nValue < 100){// 休眠50毫秒msleep(50);++nValue;// 准备更新emit resultReady(nValue);}}signals:void resultReady(int value);
};

构建一个主界面 - 包含按钮、进度条,当点击“开始”按钮时,启动线程,更新进度条。

class MainWindow : public CustomWindow
{Q_OBJECTpublic:explicit MainWindow(QWidget *parent = 0): CustomWindow(parent){qDebug() << "Main Thread : " << QThread::currentThreadId();// 创建开始按钮、进度条QPushButton *pStartButton = new QPushButton(this);m_pProgressBar = new QProgressBar(this);//设置文本、进度条取值范围pStartButton->setText(QString::fromLocal8Bit("开始"));m_pProgressBar->setFixedHeight(25);m_pProgressBar->setRange(0, 100);m_pProgressBar->setValue(0);QVBoxLayout *pLayout = new QVBoxLayout();pLayout->addWidget(pStartButton, 0, Qt::AlignHCenter);pLayout->addWidget(m_pProgressBar);pLayout->setSpacing(50);pLayout->setContentsMargins(10, 10, 10, 10);setLayout(pLayout);// 连接信号槽connect(pStartButton, SIGNAL(clicked(bool)), this, SLOT(startThread()));}~MainWindow(){}private slots:// 更新进度void handleResults(int value){qDebug() << "Handle Thread : " << QThread::currentThreadId();m_pProgressBar->setValue(value);}// 开启线程void startThread(){WorkerThread *workerThread = new WorkerThread(this);connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));// 线程结束后,自动销毁connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater()));workerThread->start();}private:QProgressBar *m_pProgressBar;WorkerThread m_workerThread;
};

显然,UI界面、Worker构造函数、槽函数处于同一线程(主线程),而run()函数处于另一线程(次线程)。

Main Thread : 0x34fc
Worker Thread : 0x34fc
Worker Run Thread : 0x4038
Handle Thread : 0x34fc

由于信号与槽连接类型默认为“Qt::AutoConnection”,在这里相当于“Qt::QueuedConnection”。

也就是说,槽函数在接收者的线程(主线程)中执行。

注意:信号与槽的连接类型,请参考:Qt之Threads和QObjects中“跨线程的信号和槽”部分。

线程休眠

上述示例中,通过在run()函数中调用msleep(50),线程会每隔50毫秒让当前的进度值加1,然后发射一个resultReady()信号,其余时间什么都不做。在这段空闲时间,线程不占用任何的系统资源。当下一次CPU时钟来临时,它会继续执行。

QThread提供了静态的、平台独立的休眠函数:sleep()、msleep()、usleep(),允许秒,毫秒和微秒来区分,函数接受整型数值作为参数,以表明线程挂起执行的时间。当休眠时间结束,线程就会获得CPU时钟,将继续执行它的指令。

想象一下,日常用的电脑,如果我们需要离开一段时间,可以将它设置为休眠状态,为了节约用电,同时响应国家政策 - 走绿色、环保之道。

可以尝试注释掉休眠部分的代码,这时,由于没有任何耗时操作,会造成频繁地更新UI。所以,为了保证界面的流畅性,同时确保进度的更新在人眼可接受的范围内,我们应在必要的时候加上适当时间的休眠。

在主线程中更新UI

当连接方式更改为“Qt::DirectConnection”时:

connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)), Qt::DirectConnection);

再次点击“开始”按钮,会很失望,因为它会出现一个异常,描述如下:

ASSERT failure in QCoreApplication::sendEvent: “Cannot send events to objects owned by a different thread. Current thread e346e8. Receiver customWidget’ (of type ‘MainWindow’) was created in thread 4186a0”, file kernel\qcoreapplication.cpp, line 553

显然,UI界面、Worker构造函数处于同一线程(主线程),而run()函数、槽函数处于同一线程(次线程)。

Main Thread : 0x2c6c
Worker Thread : 0x2c6c
Worker Run Thread : 0x4704
Handle Thread : 0x4704

之所以会出现这种情况是因为Qt做了限制(其它大多数GUI编程也一样),不允许在其它线程(非主线程)中访问UI控件,这么做主要是怕在多线程环境下对界面控件进行操作会出现不可预知的情况。

所以,不难理解,由于在槽函数(次线程)中更新了UI,所以,会引起以上错误。

 

优雅地结束线程

如果一个线程运行完成,就会结束。可很多情况并非这么简单,由于某种特殊原因,当线程还未执行完时,我们就想中止它。

不恰当的中止往往会引起一些未知错误。比如:当关闭主界面的时候,很有可能次线程正在运行,这时,就会出现如下提示:

QThread: Destroyed while thread is still running

这是因为次线程还在运行,就结束了UI主线程,导致事件循环结束。这个问题在使用线程的过程中经常遇到,尤其是耗时操作。

在此问题上,常见的两种人:

  • 直接忽略此问题。
  • 强制中止 - terminate()。

大多数情况下,当程序退出时,次线程也许会正常退出。这时,虽然抱着侥幸心理,但隐患依然存在,也许在极少数情况下,就会出现Crash。

正如前面提到过terminate(),比较危险,不鼓励使用。线程可以在代码执行的任何点被终止。线程可能在更新数据时被终止,从而没有机会来清理自己,解锁等等。。。总之,只有在绝对必要时使用此函数。

举一个简单的例子:当你的哥们正处于长时间的酣睡状态时,你想要叫醒他,但是采取的措施却是泼一盆凉水,想象一下后果?这凉爽 - O(∩_∩)O哈哈~。

所以,我们应该采取合理的措施来优雅地结束线程,一般思路:

  1. 发起线程退出操作,调用quit()或exit()。
  2. 等待线程完全停止,删除创建在堆上的对象。
  3. 适当的使用wait()(用于等待线程的退出)和合理的算法。

下面介绍两种方式:

  • QMutex互斥锁 + bool成员变量。

这种方式是Qt4.x中比较常用的,主要是利用“QMutex互斥锁 + bool成员变量”的方式来保证共享数据的安全性(可以完全参照下面的requestInterruption()源码写法)。

#include <QThread>
#include <QMutexLocker>class WorkerThread : public QThread
{Q_OBJECTpublic:explicit WorkerThread(QObject *parent = 0): QThread(parent),m_bStopped(false){qDebug() << "Worker Thread : " << QThread::currentThreadId();}~WorkerThread(){stop();quit();wait();}void stop(){qDebug() << "Worker Stop Thread : " << QThread::currentThreadId();QMutexLocker locker(&m_mutex);m_bStopped = true;}protected:virtual void run() Q_DECL_OVERRIDE {qDebug() << "Worker Run Thread : " << QThread::currentThreadId();int nValue = 0;while (nValue < 100){// 休眠50毫秒msleep(50);++nValue;// 准备更新emit resultReady(nValue);// 检测是否停止{QMutexLocker locker(&m_mutex);if (m_bStopped)break;}// locker超出范围并释放互斥锁}}
signals:void resultReady(int value);private:bool m_bStopped;QMutex m_mutex;
};

为什么要加锁?很简单,是为了共享数据段操作的互斥。
何时需要加锁?在形成资源竞争的时候,也就是说,多个线程有可能访问同一共享资源的时候。

当主线程调用stop()更新m_bStopped的时候,run()函数也极有可能正在访问它(这时,他们处于不同的线程),所以存在资源竞争,因此需要加锁,保证共享数据的安全性。

  • Qt5以后:requestInterruption() + isInterruptionRequested()

这两个接口是Qt5.x引入的,使用很方便:

class WorkerThread : public QThread
{Q_OBJECTpublic:explicit WorkerThread(QObject *parent = 0): QThread(parent){}~WorkerThread() {// 请求终止requestInterruption();quit();wait();}protected:virtual void run() Q_DECL_OVERRIDE {// 是否请求终止while (!isInterruptionRequested()){// 耗时操作}}
};

在耗时操作中使用isInterruptionRequested()来判断是否请求终止线程,如果没有,则一直运行;当希望终止线程的时候,调用requestInterruption()即可。

正如侯捷所言:「源码面前,了无秘密」。如果还心存疑虑,我们不妨来看看requestInterruption()、isInterruptionRequested()的源码:

void QThread::requestInterruption()
{Q_D(QThread);QMutexLocker locker(&d->mutex);if (!d->running || d->finished || d->isInFinish)return;if (this == QCoreApplicationPrivate::theMainThread) {qWarning("QThread::requestInterruption has no effect on the main thread");return;}d->interruptionRequested = true;
}bool QThread::isInterruptionRequested() const
{Q_D(const QThread);QMutexLocker locker(&d->mutex);if (!d->running || d->finished || d->isInFinish)return false;return d->interruptionRequested;
}

内部实现居然也用了互斥锁QMutex,这样我们就可以放心地使用了


http://chatgpt.dhexx.cn/article/G9GPurGl.shtml

相关文章

Qt之QThread介绍(常用接口及实现、自动释放内存、关闭窗口时停止线程运行、同步互斥)

在程序设计中&#xff0c;为了不影响主程序的执行&#xff0c;常常把耗时操作放到一个单独的线程中执行。Qt对多线程操作有着完整的支持&#xff0c;Qt中通过继承QThread并重写run()方法的方式实现多线程代码的编写。针对线程之间的同步与互斥问题&#xff0c;Qt还提供了QMutex…

Qt线程QThread开启和安全退出

1、线程开启 Qt中&#xff0c;开启子线程&#xff0c;一般有两种方法&#xff1a; a, 定义工作类worker: worker继承 QThread, 重写run函数&#xff0c;在主线程中实例化worker&#xff0c;把耗时工作放进worker的run函数中完成&#xff0c;结束后&#xff0c;往主线程中发信…

QThread的用法

概述 QThread类提供了一个与平台无关的管理线程的方法。一个QThread对象管理一个线程。QThread的执行从run()函数的执行开始&#xff0c;在Qt自带的QThread类中&#xff0c;run()函数通过调用exec()函数来启动事件循环机制&#xff0c;并且在线程内部处理Qt的事件。在Qt中建立线…

Oracle 定时任务执行存储过程【建议收藏】

首先用一个完整的例子来实现定时执行存储过程。 任务目标&#xff1a;每小时向test表中插入一条数据。 实现方案&#xff1a; 1.通过 oracle 中 dbms_job 完成存储过程的定时调用 2.在存储过程中完成相应的逻辑操作 实现步骤&#xff1a; 1.创建一个测试表 create table test…

【Mysql】MySQL 用户执行存储过程的权限

问题 运行存储过程报错&#xff1a; 原因 查询资料&#xff1a; 1305错误&#xff0c;由于当前用户没用权限&#xff0c;对用户进行授权后可以执行。 解决 MySQL创建存储过程/函数需要的权限&#xff1a; alter routine---修改与删除存储过程/函数 create routine--创建…

goland 使用 gorm 执行 存储过程 : go语言 执行存储过程

使用 gorm 执行 存储过程 初安装依赖代码&#xff1a; 附存储过程图片存储过程代码&#xff08;创建&#xff09; 表结构表结构图表结构代码 初 最近遇到要写存储过程需求&#xff0c;使用 大佬写的 框架 gorm 来完成。简直是方便的不行&#xff1a; 直接上代码&#xff1a; …

JDBC之CallableStatement执行存储过程

​ 在前面的一篇文章中&#xff0c;我们学习使用Statement、PreparedStatement来完成对数据表的增删改查。而存储过程作为数据库的重要组成部分&#xff08;痛点&#xff0c;当时学的时候头发都掉了好几根&#x1f62d;&#xff09;&#xff0c;那JDBC是如何执行存储过程呢&…

mysql创建定时任务执行存储过程

存储过程已添加好&#xff1a;https://blog.csdn.net/YXWik/article/details/127283316 1.创建定时器用来执行存储过程函数 create event delete_data on schedule every 10 second do call delete_data();这里的第一行代表的创建名称为delete_data的事件 第二行是执行周期为…

Oracle 定时任务执行存储过程

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、创建存储过程一、创建定时任务 一、创建存储过程 CREATE OR REPLACE PROCEDURE TESTCREATE AS --存储过程名称I INT : 1; --变量 BEGINWHILE I < 10 LOOP -…

oracle执行存储过程参数,Oracle 执行存储过程四种方法(带参数 不带参数)

1.如果是命令窗口就用exec 存储过程名&#xff1a; 1 EXEC procedure;--procedure是存储过程名 2.如果是 SQL窗口就用 begin 存储过程名 end; 1 2 3 begin procedure;--procedure是存储过程名 end; 3.如果是程序中调用就用 call 存储过程名 &#xff0c;举个栗子&#xff1…

plsql定时执行存储过程

1、创建定时向表中插入数据的存储过程&#xff0c;名为testJob。 CREATE OR REPLACE PROCEDURE "testJob" AS BEGIN EXECUTE IMMEDIATE INSERT INTO TABLE_HIS SELECT * FROM TABLE_AI; COMMIT; END; 2、使用plsql找到定时器对应的DBMS_Jobs文件夹&#xff0c;…

SQLserver存储过程简单写法与设置定时执行存储过程方法

最近工作中需要写SQLserver的存储过程&#xff0c;第一次使用&#xff0c;简单记录下&#xff0c;以防遗忘。 在SQLserver可视化工具中编写&#xff0c;我的工具如下图&#xff1a; 首先点击你的数据库&#xff0c;找到可编程性&#xff0c;在可编程性里面右击存储过程-->点…

symlink() 函数

查看更多 https://www.yuque.com/docs/share/10f959a4-bd7e-47a9-ad78-11a1310613f3

Install fail! Error: EPERM: operation not permitted, symlink

这个是在安装lodash的时候报错的情况 尝试了下方命令&#xff0c;但是还是有误 在这里可能是因为npm缓存的问题&#xff0c;我先删除了 C:\Users\abc(自己电脑的用户名) 文件夹下的 .npmrc 文件&#xff0c;但是没反应。然后运行了下方的命令&#xff1a; 运行&#xff1a;…

error: eperm: operation not permitted, symlink

查询当前配置的镜像 npm get registry > https://registry.npmjs.org/ 设置成淘宝镜像 npm config set registry http://registry.npm.taobao.org/

Linux Symbolic Links(软链接)

linux 中的链接有两种&#xff0c;分别是 Hard Links&#xff08;硬链接&#xff09;和Symbolic Links(软链接)。 1. 什么是硬链接、软链接&#xff1f; 硬链接&#xff1a;在另外一个位置创建源文件的链接文件&#xff0c;相当于复制了一份&#xff0c;占用资源会倍增。硬链…

linux link/symlink/unlink 硬连接和软连接介绍

文章目录 硬连接和软连接的区别硬连接软连接 链接命令lnln指令参数含义例子 硬链接函数link()符号链接函数symlink()解除链接函数unlink() link/symlink/unlink函数头文件为#include <unistd.h> 硬连接和软连接的区别 硬连接 硬连接指通过索引节点来进行连接。 在Lin…

​2021-12-13 Android 的 init.rc 文件​里面的symlink,把<target>链接到目录<path>下。

一、symlink <target> <path>&#xff0c;把<target>链接到目录<path>下。 二、来看一下实际的例子 三、参考文章&#xff1a; Android 的 init.rc 文件简介 &#xff08;转&#xff09; - 陈wei的个人空间 - OSCHINA - 中文开源技…

pnpm安装使用教程以及pnpm node版本管理以及EPERM operation not permitted symlink问题解决

pnpm安装使用教程以及pnpm node版本管理以及EPERM operation not permitted symlink问题解决 pnpm&#xff1a;目前来看是一个大趋势&#xff0c;如&#xff1a;nest-cli目前除了npm、yarn增加了pnpm 优势&#xff1a;节约磁盘空间并提升安装速度&#xff08;改善node_modules…

Cannot create symlink/symbolic to `xxx': Operation not supported

1、Cannot create symlink to xxx: Operation not supported 在虚拟机的共享目录中&#xff0c;解压内核源码&#xff0c;试过了几种解压方法都报出如下错误&#xff1a; tar: linux-4.15/tools/testing/selftests/powerpc/vphn/vphn.h: Cannot create symlink to ../../../.…