【QT】一文学会 QT 多线程(QThread )

server/2025/3/31 6:05:31/

一、Qt 多线程概述

在 Qt 中,多线程的处理一般是通过 QThread类 来实现。

  • QThread 代表一个在应用程序中可以 独立控制 的线程,也可以和进程中的其他线程共享数据
  • QThread 对象管理程序中的一个控制线程。

创建线程的两种方式

① 使用 QThread 类
 QThread 类是 Qt 中实现多线程的基础类之一,通过继承 QThread 类并重写其 run() 函数可以实现自定义线程逻辑。
线程类

#ifndef WORKER_H
#define WORKER_H#include <QThread>class Worker : public QThread
{
public:Worker();void run();void printFunc();};#endif // WORKER_H

实现如下:

#include "Worker.h"
#include <QDebug>Worker::Worker()
{}void Worker::run()
{qDebug()<<"子线程ThreadID: "<<QThread::currentThreadId();
}void Worker::printFunc()
{qDebug()<<"子线程成员函数ThreadID: "<<QThread::currentThreadId();
}

main 函数

#include <iostream>
#include <QDebug>
#include "Worker.h"using namespace std;int main()
{Worker w;w.start();qDebug()<<"主线程ThreadID: "<<QThread::currentThreadId();w.printFunc();while (1){}return 0;
}

结果分析:

  • 主线程和子线程执行的顺序不确定,偶尔主线程在前,偶尔子线程在前。
  • 子线程类的成员函数包括槽函数是运行在主线程当中的,只有run()函数运行在子线程中。
  • 如果在run()函数中调用子线程类成员函数,那么该成员函数运行在子线程中。

② 使用 moveToThread()
moveToThread() 是 Qt 中用于将对象移动到另一个线程的方法。通过调用 moveToThread() 函数,可以将一个 QObject 对象从当前线程移动到另一个线程中,从而实现对象在新线程中执行特定的任务。

    在多线程编程中,通常会使用 moveToThread() 方法来将耗时的任务或需要在单独线程中执行的逻辑移动到单独的线程中,以避免阻塞主线程(通常是 GUI 线程)的执行。

线程类:

#ifndef WORKER_H
#define WORKER_H#include <QObject>class Worker : public QObject
{Q_OBJECT
public:Worker();void printFunc();public slots:void doWork();void doWork2();void doWork3();signals:void testdoWork3();};#endif // WORKER_H

实现如下:

#include "Worker.h"
#include <QDebug>
#include <QThread>Worker::Worker()
{}void Worker::printFunc()
{qDebug() << "成员函数ThreadID:"<<QThread::currentThreadId();}void Worker::doWork()
{qDebug() << "doWork ThreadID:"<<QThread::currentThreadId();
}void Worker::doWork2()
{qDebug() << "doWork2 ThreadID:"<<QThread::currentThreadId();
}void Worker::doWork3()
{qDebug() << "doWork3 ThreadID:"<<QThread::currentThreadId();
}

main 函数

#include "mainwindow.h"#include <QApplication>
#include "Worker.h"
#include <QDebug>
#include <QThread>int main(int argc, char *argv[]) {QApplication a(argc, argv);//MainWindow w;//w.show();Worker worker;QThread thread;worker.moveToThread(&thread);QObject::connect(&thread, &QThread::started, &worker, &Worker::doWork);         //第一槽函数QObject::connect(&thread, &QThread::started, &worker, &Worker::doWork2);        //第二槽函数QObject::connect(&worker, &Worker::testdoWork3, &worker, &Worker::doWork3);     //第三槽函数//启动线程thread.start();//调用成员数worker.printFunc();//发送自定义信号emit worker.testdoWork3();while (1) {}return a.exec();
}

结果分析:

  • 槽函数无论是线程的信号触发还是自定义信号触发,槽函数都在新线程里运行。
  • 成员函数和主函数运行在主线程当中。

对比 QThread 和 moveToThread()

① QThread 方式:

使用场景:

  • 当需要创建一个独立的线程来执行某个任务,且需要对线程的整个生命周期进行管理时,适合使用 QThread 方式。
  • 当任务逻辑相对简单或独立,不需要频繁地进行线程间通信时,可以选择使用 QThread 方式。

优点:

  • 可以直接控制线程的生命周期,包括启动、停止、等待线程退出等。
  • 适合单一任务的线程处理,结构相对清晰易懂。
  • 相对直观,可以比较容易理解和使用。

缺点:

  • 需要手动管理线程之间的通信和数据共享,容易引入线程安全问题。
  • 繁琐的线程管理和同步机制可能增加代码复杂度和风险。

② moveToThread() 方式:

使用场景:

  • 当需要将一个 QObject 对象移动到指定的线程中执行任务,或者需要多个对象在同一线程中协同工作时,适合使用 moveToThread() 方式。
  • 当需要灵活地控制对象和线程之间的关系,进行复杂的线程间通信时,可以选择使用 moveToThread() 方式。

优点:

  • 可以利用信号和槽机制方便地实现对象在不同线程中的通信。
  • 可以更灵活地管理对象和线程的关系,避免直接操作线程带来的问题。
  • 适合处理复杂的多线程通信和任务分发。

缺点:

  • 无法直接控制线程的启动和停止,线程的生命周期由对象决定,可能使得线程管理稍显复杂。
  • 对对象的线程移动可能引入一些额外的开销,需要谨慎设计线程之间的交互逻辑。

总结:
选择使用 QThread 或 moveToThread() 方式创建线程取决于具体需求和情况。可以根据以下原则进行选择:

  1. 如果需要独立管理整个线程的生命周期、简单的多线程操作,并且不涉及复杂的线程间通信,可以选择 QThread 方式。
  2. 如果需要灵活地管理对象与线程之间的关系、复杂的多线程通信和任务分发,可以选择 moveToThread() 方式。

综上所述,根据项目需求、任务复杂度和开发方便性来选择适合的创建线程方式

二、QThread 常用 API

方法名作用
run()线程入口函数
start()通过调用 run() 开始执行线程,操作系统会根据 优先级参数调度 线程。如果线程正在运行,则这个函数什么都不会做
currentThread()返回一个指向管理当前执行线程的 QThread 的指针
isRunning()如果线程正在运行返回 true,否则反之
sleep() / msleep() / usleep()使线程休眠,单位为 秒/毫秒/微秒
wait()阻塞线程,直到满足以下任何一个条件:
与此 QThread 对象关联的线程已经完成执行(即当它从run()返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。
已经过了几毫秒。如果时间是 ULONG MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回false。
这提供了与 POSIX pthread_join()函数类似的功能。
terminate()终止线程执行。(可以选择立即终止,也可以不立即终止)
取决于操作系统的 调度策略,在 terminate() 之后使用 QThread::wait() 来确保
finished()当线程结束时会发出该信号,通过其来实现线程清理工作

在使用QThread类中的常用函数时,有一些注意事项需要注意:

  • start() 函数:调用start()函数启动线程时,会自动调用线程对象的run()方法。不要直接调用run()方法来启动线程,应该使用start()函数。
  • wait() 函数:wait()函数会阻塞当前线程,直到线程执行完成。在调用wait()函数时需要确保不会发生死锁的情况,避免主线程和子线程相互等待对方执行完成而无法继续。
  • terminate() 函数:调用terminate()函数会强制终止线程,这样可能会导致资源未能正确释放,造成内存泄漏等问题。因此应该尽量避免使用terminate()函数,而是通过设置标志让线程自行退出。
  • quit() 函数:quit()函数用于终止线程的事件循环,通常与exec()函数一起使用。在需要结束线程事件循环时,可以调用quit()函数。
  • finished 信号:当线程执行完成时会发出finished信号,可以连接这个信号来处理线程执行完成后的操作。
  • yieldCurrentThread() 函数:yieldCurrentThread()函数用于让当前线程让出时间片,让其他线程有机会执行。使用时应该注意避免过多的调用,否则会影响程序性能。

三、线程使用 – 倒计时

【实现倒计时页面】

1、创建项目,以 Widget 作为基类,设计 UI 界面如下:

image-20250131195525774

2、新建一个类,并且继承于 QThread 类

image-20250131195801401

3、thread.h 声明实现如下:

image-20250131200711715

run 方法 在 thread.cpp 实现如下:

void Thread::run()
{// 这里我们不能直接去修改界面内容// 原因:存在线程安全问题,Qt 针对界面的控件状态的任何修改必须在 主函数 中进行// 这里我们就仅仅针对时间进行计时即可// 每隔一秒 通知主线程更新界面内容for(int i = 0; i < 10; i++){sleep(1);// 发送一个信号 通知主线程emit notify();}
}

4、Widget 声明实现如下:

image-20250131201008609

Widget.cpp 代码如下:

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 连接信号槽,通过槽函数更新界面connect(&thread, &Thread::notify, this, &Widget::handle);// 启动线程thread.start();
}void Widget::handle()
{// 此处修改界面内容int val = ui->lcdNumber->intValue();val--;ui->lcdNumber->display(val);
}
  • 演示这里就省略了,大家可以自行演示结果

💡 说明:

  1. 线程函数内部不允许操作 UI 图形界面,一般用数据处理;
  2. connect()函数第五个参数表示的为连接的方式,且只有在多线程的时候才意义。

connect()函数第五个参数为 Qt::ConnectionType,用于指定信号和槽的连接类型。同时影响信号的传递方式和槽函数的执行顺序。

Qt::ConnectionType 提供了以下五种方式:

名称作用
Qt::AutoConnection在 Qt中,会根据信号和槽函数所在的线程 自动选择 连接类型。如果信号和槽函数在同一线程中,那么使用 Qt:DirectConnection 类型;如果它们位于不同的线程中,那么使用Ot::QueuedConnection 类型
Qt::DirectConnection当信号发出时,槽函数会 立即在同一线程中执行 。这种连接类型适用于信号和槽函数在同一线程中的情况,可以实现直接的函数调用,但需要注意线程安全性
Qt::QueuedConnection当信号发出时,槽函数会被插入到接收对象所属的线程的事件队列中,等待下一次事件循环时执行。这种连接类型适用于信号和槽函数在不同线程中的情况,可以确保线程安全
Qt::BlockingQueuedConnectionQt:QueuedConnection 类似,但是发送信号的线程会被 阻塞,直到函数执行完毕,这种连接类型适用于需要等待槽函数执行完毕再继续的场景,但需要注意可能引起 线程死锁 的风险。
Qt::UniqueConnection这是一个标志,可以使用 位或 与上述任何一种连接类型组合使用

四、线程安全

实现线程互斥和同步常用的类有:

  • 互斥锁:QMutex、QMutexLocker.
  • 条件变量:QWaitCondition
  • 信号量:QSemaphore
  • 读写锁:QReadLocker、QWriteLocker、OReadWriteLock

1. 互斥锁 QMutex & QMutexLocker

互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在Qt中,互斥锁主要是通过 QMutex 类来处理。

  • QMutex 是 Qt 中用于实现互斥锁的类,用于保证在多线程程序中访问共享资源的互斥性。它提供了两个基本操作:lock() 和 unlock(),分别用于加锁和解锁。
  • QMutexLocker 是 QMutex 的 RAII 风格封装,可以自动释放锁资源,避免忘记解锁而导致的死锁情况。QMutexLocker 在创建时会自动调用 QMutex 的 lock() 方法,析构时会自动调用 QMutex 的 unlock() 方法。因此使用 QMutexLocker 可以大大减少忘记解锁的情况。
QMutex
  • 特点:QMutex 是Qt框架提供的互斥锁类,用于 保护共享资源 的访问,实现线程间的互斥操作。
  • 用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex;
mutex.lock(); //上锁
//访问共享资源
//...
mutex.unlock(); //解锁

【案例:多线程自加】

先创建一个项目, 以 QWidget 作为基类,然后再创建C++ Class文件 Thread(新建一个类,并且继承于 QThread 类),和线程使用那步骤类似,如下:

声明如下

image-20250201152005421

run 方法 在 Thread.cpp 实现如下:

// 头文件既要声明,.cpp 文件也要定义
int Thread::num = 0;
QMutex Thread::mutex;void Thread::run()
{for(int i = 0; i < 50000; i++){mutex.lock();num++;mutex.unlock();}
}

Widget.cpp 构造函数如下:

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 创建两个线程对象Thread t1, t2;t1.start();t2.start();// 加上两个线程,让主线程等待两线程结束t1.wait();t2.wait();// 打印结果qDebug() << Thread::num;
}

最后结果不出意外:100000,如果我们不加锁的话,就会出现意外,可以自行测试

QMutexLocker
  • 特点:QMutexLockerQMutex 的辅助类,使用 RAII(Resource Acquisition ls Initialization) 方式对互斥锁进行 上锁和解锁 操作。
  • 用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁 等问题。

C++ 中引入 智能指针 就是解决这个问题的,C++ 11 引入了 std::lock_guard 相当于是 std::mutex 智能指针,借助了 RAII 机制

QMutex mutex;
{QMutexLocker locker(&mutex)://在作用域内自动上锁// 访间共享资源// ...
}//在作用城结束时自动解锁

如下,借助上面互斥锁的例子,做点修改

image-20250201152456305

QReadWriteLocker、QReadLocker、QWriteLocker

特点:

  • QReadWriteLock 是读写锁类,用于控制读和写的并发访问。
  • QReadLocker 用于读操作上锁,允许多个线程同时读取共享资源。
  • QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源。

**用途:**在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。

QReadWriteLock rwLock;
//在读操作中使用读锁
{QReadLocker locker(&rwLock); //在作用域内自动上读锁//读取共享资源//...
} //在作用域结束时自动解读锁//在写操作中使用写锁
{QWriteLocker locker(&rwLock); //在作用用域内自动上写锁//修改共享资源//...
} //在作用域结束时自动解写锁

2. 条件变量 QWaitCondition

在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还 必须等待某些条件满足 才能执行,这时就会出现问题。

  • 这种情况下,线程会很自然地使用锁的机制来 阻塞 其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。
  • 当条件满足时,等待条件的线程将被另一个线程唤醒。

在 Qt 中,专门提供了 QWaitCondition类 来解决像上述这样的问题。

特点:QWaitCondition 是Qt框架提供的条件变量类,用于线程之间的消息通信和同步。

用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。

QMutex mutex;
QWaitCondition condition;//在等待线程中
mutex.lock();
//检查条件是否满足,若不满足则等待
while (!conditionFullfilled())
{condition.wait(&mutex); //等待条件满足并释放锁
}//条件满足后继续执⾏
//...
mutex.unlock();//在改变条件的线程中
mutex.lock();//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
  • 注意:这里使用 while 判定 而不是 if
  • 因为唤醒之后 需要再确认一下当前条件是否真的成立,wait 可能被提前唤醒了

3. 信号量 QSemphore

有时在多线程编程中,需要确保多个线程可以相应的 访问一个数量有限的相同资源

例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用 信号量 来处理。

信号量类似于 增强 的互斥锁(Plus 版),不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量

  • 特点:QSemaphore 是 Qt框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
  • 用途:限制并发线程数量,用于解决一些资源有限的问题。
  • QSemaphore 是 Qt 中用于实现信号量的类,用于控制对共享资源的访问数量。它可以用来限制同时访问某一资源的线程数量,也可以用于线程之间的同步
  • QSemaphore 可以被获取和释放,当信号量的值为正时,线程可以获得该信号量;当信号量的值为零时,线程将被阻塞,直到有线程释放信号量。
  • 通过获取和释放信号量,可以实现线程之间的协调和资源的管理。
QSemaphore semaphore(2); //同时允许两个线程访问共享资源//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞//访问共享资源
//...semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作

代码示例如下:

#include <QCoreApplication> 
#include <QThread> 
#include <QDebug> 
#include <QSemaphore>QSemaphore semaphore(2); // 定义能够同时访问资源的线程数量为2的信号量class MyThread : public QThread // 定义一个线程类
{
public:void run() override {if(semaphore.tryAcquire()) { // 尝试获取信号量qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Acquired Semaphore"; // 输出线程ID和已获取信号量消息sleep(2); // 线程休眠2秒qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Releasing Semaphore"; // 输出线程ID和释放信号量消息semaphore.release(); // 释放信号量} else {qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Semaphore not acquired"; // 输出线程ID和未获取信号量消息}}
};int main(int argc, char *argv[]) // 主函数
{QCoreApplication a(argc, argv); // 创建应用程序对象MyThread thread1; // 创建线程对象1MyThread thread2; // 创建线程对象2MyThread thread3; // 创建线程对象3thread1.start(); // 启动线程1thread2.start(); // 启动线程2thread3.start(); // 启动线程3thread1.wait(); // 等待线程1结束thread2.wait(); // 等待线程2结束thread3.wait(); // 等待线程3结束return a.exec(); // 执行应用程序事件循环
}

http://www.ppmy.cn/server/178665.html

相关文章

单片机自学总结

自从工作以来&#xff0c;一直努力耕耘单片机&#xff0c;至今&#xff0c;颇有收获。从51单片机&#xff0c;PIC单片机&#xff0c;直到STM32&#xff0c;以及RTOS和Linux&#xff0c;几乎天天在搞:51单片机&#xff0c;STM8S207单片机&#xff0c;PY32F003单片机&#xff0c;…

掌握XXL-JOB:快速搭建高效任务调度系统

一、前言 定时任务作为自动化执行的核心机制&#xff0c;指系统按预设时间或周期触发特定操作&#xff0c;广泛应用于数据同步&#xff08;如每日报表生成&#xff09;、状态更新&#xff08;如订单超时关闭&#xff09;等场景。 在分布式架构与微服务盛行的当下&#xff0c;…

vscode python 入门教程(二) vscode使用gti 管理代码

vscode 代码管理需要用管道git的命令&#xff0c;这点和idea的代码管理区别比较大。 作为java开发需要自己熟悉适应一下。 一、GitHub 新建一个仓库 过程略 二、本地git 项目初始化 git initvscode 中可以看到 文件状态 git status使用git remote 命令吧本地git 仓库和远程…

本地部署 Firecrawl

本地部署 Firecrawl 本文档概述了如何本地部署 Firecrawl。 为什么要本地部署&#xff1f; 增强安全性和合规性&#xff1a; 数据处理和存储完全在您的控制之下&#xff0c;符合内部和外部法规。Firecrawl 作为 Mendable 产品&#xff0c;依赖于 SOC2 Type2 认证&#xff0c;…

2025年渗透测试面试题总结-某深信服 -安全工程师(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 深信服 -安全工程师 一、宽字节注入原理与编码影响 &#xff08;一&#xff09;技术原理 &#xff08…

基于AWS Endpoint Security(EPS)的全天候威胁检测与响应闭环管理

设计AWS云架构方案实现基于AWS Endpoint Security(EPS)的全天候威胁检测与响应&#xff0c;使用EPS通过代理实时监控终端进程、网络连接等行为&#xff0c;例如检测异常登录尝试或恶意软件活动。一旦发现威胁&#xff0c;系统会自动生成安全事件工单并触发响应流程&#xff0c;…

Js闭包Closure 及 其可能产生的内存泄漏问题

闭包的主要作用&#xff1a;实现数据私有&#xff0c;函数内定义的私有变量&#xff0c;外面可以使用访问&#xff0c;但不可以修改。 以统计调用次数的函数为例&#xff1a; 普通形式&#xff1a; let a 0;function fn() {a;console.log(a: ${a});} 闭包形式&#xff1a; …

批量给 PPT 幻灯片页面添加文字和图片水印

给 PPT 文档添加水印是非常常见的一个操作&#xff0c;不仅可以保护我们的版权&#xff0c;也可以起到很好的宣传作用。那如何给 PPT 文档地幻灯片页面添加水印呢&#xff1f;如何添加文字水印或者图片水印呢&#xff1f;今天就给大家介绍一种批量给 PPT 添加水印的方法。 在网…