引言

前面几篇已经对C++的线程做了简单的总结,浅谈C++11中的多线程(三) - 唯有自己强大 - 博客园 (cnblogs.com)。本篇着重于Qt多线程的总结与实现。

跟C++11中很像的是,Qt中使用QThread来管理线程,一个QThread对象管理一个线程,在使用上有很多跟C++11中相似的地方,但更多的是Qt中独有的内容。另外,QThread对象也有消息循环exec()函数,即每个线程都有一个消息循环,用来处理自己这个线程的事件。


一,知识回顾

首先先来回顾一下一些知识点:

1,为什么需要多线程?

解决耗时操作堵塞整个程序的问题,一般我们会将耗时的操作放入子线程中

2,进程和线程的区别:

进程:一个独立的程序,拥有独立的虚拟地址空间,要和其他进程通信,需要使用进程通信的机制。

线程:没有自己的资源,都是共享进程的虚拟地址空间,多个线程通信存在隐患。

ps:在操作系统每一个进程都拥有独立的内存空间,线程的开销远小于进程,一个进程可以拥有多个线程。(因此我们常用多线程并发,而非多进程并发)

为了更容易理解多线程的作用,先看一个实例:

在主线程中运行一个10s耗时的操作。(通过按钮来触发)

void Widget::on_pushButton_clicked()
{
QThread::sleep(10);
}

可以看到程序运行过程中,整个线程都在响应10秒的耗时操作,对于线程的消息循环exec()函数就未响应了(就是你在这个过程中拖动界面是无反应的)

 二,Qt中实现多线程的两种方法

2.1.派生QThread类对象的方法(重写Run函数)

首先,以文字形式来说明需要哪几个步骤。

  1. 自定义一个自己的类,使其继承自QThread类;
  2. 在自定义类中覆写QThread类中的虚函数run()。

这很可能就是C++中多态的使用。补充一点:QThread类继承自QObject类。

这里要重点说一下run()函数了。它作为线程的入口,也就是线程从run()开始执行,我们打算在线程中完成的工作都要写在run()函数中,个人认为可以把run()函数理解为线程函数。这也就是子类覆写基类的虚函数,基类QThread的run()函数只是简单启动exec()消息循环,关于这个exec()后面有很多东西要讲,请做好准备。
那么我们就来尝试用多线程实现10s耗时的操作:(用按钮触发)

1️⃣在编辑好ui界面后,先创建一个workThread1的类。(继承自QThread类(可以先继承Qobject再去改成QThread))

2️⃣在workThread1的类中重写run函数

在workThread1.h的public类声明run函数: void run();

在workThread1.cpp中重写run函数(打印子线程的ID):

#include "workthread1.h"
#include<QDebug>
workThread1::workThread1(QObject *parent) : QThread(parent)
{ }
//重写run函数
void workThread1::run()
{
qDebug()<<"当前线程ID:"<<QThread::currentThreadId();
qDebug()<<"开始执行线程";
QThread::sleep(10);
qDebug()<<"线程结束"; }

3️⃣在widget.cpp中的button的click事件中打印主线程ID:

void Widget::on_pushButton_clicked()
{
qDebug()<<"当前线程ID:"<<QThread::currentThreadId();
}

4️⃣启动子线程

在widget.h的private中声明线程 workThread1 *thread1;(需添加#include<workthread1.h>)

在widget.cpp中初始化该线程,并启动:

#include "widget.h"
#include "ui_widget.h"
#include<QThread>
#include<QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
thread1=new workThread1(this);//初始化子线程 } Widget::~Widget()
{
delete ui;
} void Widget::on_pushButton_clicked()
{
qDebug()<<"当前线程ID:"<<QThread::currentThreadId();
thread1->start();//启动子线程
}

可以实现,在执行耗时操作时也可拖动界面。

需要注意的是:

使用QThread::currentThreadId()来查看当前线程的ID,无论是子线程还是主线程,不同线程其ID是不同的。注意,这是一个静态函数,因此可以不经过对象来调用。

创建的workThread1类的执行实际上是在主线程里的,只有run函数内的程序才会在子线程中执行!(即QThread只是线程的管理类,只有run()才是我们的线程函数)

因此在QThread(即创建的类)中的成员变量属于主线程,在访问前需要判断访问是否安全。run()中创建的变量属于子线程。

线程之间共享内存是不安全的(由于多线程争夺资源会影响数据安全问题),解决的办法就是要上锁。


关于互斥锁

互斥锁是一种简单的加锁的方法来控制对共享资源的访问。只要某一个线程上锁了,那么就会强行霸占公共资源的访问权,其他的线程无法访问直到这个线程解锁了,从而保护共享资源。

在Qt中的互斥锁常用两种方式:

  • QMutex类下的lock(上锁)和unlcok(解锁)
//需要在头文件中引用#include<QMutex>
//并在头文件的private中声明QMutex mutex;

mutex.lock()
public_value++;//公共成员变量
mutex.unlock();
  • QMutexLocker类下的lock(上锁后,当执行析构函数时会自动解锁)
//需要在头文件中引用#include<QMutexLocker>和include<QMutex>
//并在头文件的private中声明QMutex mutex; QMutexLocker lock(&mutex);//执行构造函数时执行mutex.lock()
public_value++; //执行析构函数时执行mutex.unlock()

关于exec()消息循环

个人认为,exec()这个点太重要了,同时还不太容易理解。

比如下面的代码中有两个exec(),我们讲“一山不容二虎”,放在这里就是说,一个线程中不能同时运行两个exec(),否则就会造成另一个消息循环得不到消息。像QDialog模态窗口中的exec()就是因为在主线程中同时开了两个exec(),导致主窗口的exec()接收不到用户的消息了。但是!但是!但是!我们这里却没有任何问题,因为它们没有出现在同一个线程中,一个是主线程中的exec(),一个是子线程中的exec()。

#include <QApplication>
#include <QThread>
#include <QDebug> class MyThread:public QThread
{
public:
void run()
{
qDebug()<<"child thread begin"<<endl;
qDebug()<<"child thread"<<QThread::currentThreadId()<<endl;
QThread::sleep(5);
qDebugu()<<"QThread end"<<endl;
this->exec();
}
}; int main(int argc,char ** argv) //mian()作为主线程
{
QApplication app(argc,argv); MyThread thread; //创建一个QThread派生类对象就是创建了一个子线程
thread.start(); //启动子线程,然后会自动调用线程函数run() qDebug()<<"main thread"<<QThread::currentThreadId()<<endl;
QThread::sleep(5);
qDebugu()<<"main thread"<<QThread::currentThreadId()<<endl; thread.quit(); //使用quit()或者exit()使得子线程能够退出消息循环,而不至于陷在子线程中
thread.wait(); //等待子线程退出,然后回收资源
//thread.wait(5000); //设定等待的时间 return app.exec();
}

如果run()函数中没有执行exec()消息循环函数,那么run()执行完了也就意味着子线程退出了。一般在子线程退出的时候需要主线程去回收资源,可以调用QThread的wait(),使主线程等待子线程退出,然后回收资源。这里wait()是一个阻塞函数,有点像C++11中的join()函数。

但是!但是!但是!run()函数中调用了exec()函数,exec()是一个消息循环,也可以叫做事件循环,也是会阻塞的,相当于一个死循环使子线程卡在这里永不退出,必须调用QThread的quit()函数或者exit()函数才可以使子线程退出消息循环,并且有时还不是马上就退出,需要等到CPU的控制权交给线程的exec()。

所以先要thread.quit();使退出子线程的消息循环, 然后thread.wait();在主线程中回收子线程的资源。

值得注意的有两点:子线程的exet()消息循环必须在run()函数中调用;如果没有消息循环的话,则没有必要调用quit( )或者exit(),因为调用了也不会起作用。

第一种创建线程的方式需要在run()中显式调用exec(),但是exec()有什么作用呢,目前还看不出来,需要在第二种创建线程的方式中才能知道。


2.2.使用信号与槽方式来实现多线程

刚讲完使用QThread派生类对象的方法创建线程,现在就要来说它一点坏话。这种方法存在一个局限性,只有一个run()函数能够在线程中去运行,但是当有多个函数在同一个线程中运行时,就没办法了,至少实现起来很麻烦。所以,当当当当,下面将介绍第二种创建线程的方式:使用信号与槽的方式,也就是把在线程中执行的函数(我们可以称之为线程函数)定义为一个槽函数。

仍然是首先以文字形式说明这种方法的几个步骤。

注意:必须通过发射信号来让槽函数在子线程中执行,发射的信号存放在子线程消息队列中。要知道发射的信号会经过一个包装,记录其发送者和接收者等信息,操作系统会根据该信号的接收者将信号放在对应线程的消息队列中。

  1. 继承QObject来自定义一个类,该类中实现一个槽函数,也就是线程函数,实现线程要完成的工作;
  2. 在主线程(main函数)中实例化一个QThread对象,仍然用来管理子线程;
  3. 用继承自QObject的自定义类来实例化一个对象,并通过moveToThread将自己放到线程QThread对象中;
  4. 使用connect()函数链接信号与槽,因为一会儿线程启动时会发射一个started()信号;
  5. 调用QThread对象的start()函数启动线程,此时会发出一个started()信号,然后槽函数就会在子线程中执行了。

代码实例:

1️⃣在编辑好ui界面后,先创建一个workThread1的类。(继承自QThread类),并定义槽函数(子线程执行的程序都可以放在槽函数中)

//workThread1.cpp(现在workThread1.h中声明槽函数)

void workThread1:: doWork()
{
qDebug()<<"当前线程ID:"<<QThread::currentThreadId();
qDebug()<<"开始执行";
QThread::sleep(10);
qDebug()<<"结束执行";
}

2️⃣再主线程中(widget.cpp)实例化一个QThread对象thread。

 //需要引用#include<QThread>
QThread *thread=new QThread();

3️⃣在workThread1的类中实例化一个对象thread1,并通过moveToThread将自己放到线程QThread对象中

采用在widget.h中声明,在widget中实例化(上面的实例化是直接实例化,这里需要把thread1声明在private中了)

  //widget.h中的private

workThread1 *thread1;
  //widget.cpp中

  thread1=new workThread1(this);//初始化
thread1->moveTOThread(thread);//将自定义的类的对象放入线程QThread对象中

4️⃣在按钮的click事件中中打印主线程ID。

void Widget::on_pushButton_clicked()
{
qDebug()<<"当前线程ID(主线程):"<<QThread::currentThreadId();
}

5️⃣在widget.cpp中将按钮事件(信号)连接槽函数(即子线程),并运行线程thread。

在运行槽函数时,不能在此直接调用(如:thread1->doWork())。应该使用信号与槽的方法(即用connect连接)

#include "widget.h"
#include "ui_widget.h"
#include<QThread>
#include<QDebug> Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//不能指定自定义类的对象的父类为widget,即没有this(很重要!!!!)
thread1=new workThread1();//初始化
QThread *thread=new QThread(this);
thread1->moveToThread(thread);
//线程结束时清理线程内存
connect(thread,&QThread::finished,thread,&QThread::deleteLater);
//将按钮事件(信号)绑定槽函数
connect(ui->pushButton,&QPushButton::clicked,thread1,&workThread1::doWork);
//线程启动
thread->start();
} Widget::~Widget()
{
delete ui;
} void Widget::on_pushButton_clicked()
{
qDebug()<<"当前线程ID(主线程):"<<QThread::currentThreadId(); }

也可以实现,在执行耗时操作时也可拖动界面。

一般来说(这些程序都是要放在workThread1中的)

workThread1::workThread1(QObject *parent) : QObject(parent)
{
QThread *thread=new QThread(this);
moveToThread(thread);
connect(thread,&QThread::finished,thread,&QThread::deleteLater);
thread->start();
}

在主程序运行:

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
thread1=new workThread1();//初始化
connect(ui->pushButton,&QPushButton::clicked,thread1,&workThread1::doWork); }

特别需要注意的是(爬坑记录):

一号坑:子线程中操作UI

Qt创建的子线程中是不能对UI对象进行任何操作的,即QWidget及其派生类对象,这个是我掉的第一个坑。可能是由于考虑到安全性的问题,所以Qt中子线程不能执行任何关于界面的处理,包括消息框的弹出。正确的操作应该是通过信号槽,将一些参数传递给主线程,让主线程(也就是Controller)去处理。
 
二号坑:自定义的类不能指定父对象
比如上面程序中的:(不能指定自定义类对象为widget,即不可以加this)

thread1=new workThread1();//初始化

 三号坑:信号的参数问题

 这个就实属有毒,搞了我好久。这个涉及到了Qt的元对象系统(Meta-Object System)和信号槽机制。
元对象系统即是提供了Qt类对象之间的信号槽机制的系统。要使用信号槽机制,类必须继承自QObject类,并在私有声明区域声明Q_OBJECT宏。当一个cpp文件中的类声明带有这个宏,就会有一个叫moc工具的玩意创建另一个以moc开头的cpp源文件(在debug目录下),其中包含了为每一个类生成的元对象代码。

在使用connect函数的时候,我们一般会把最后一个参数忽略掉。这时候我们需要看下函数原型:

[static] QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)

可以看到,最后一个参数代表的是连接的方式。

我们一般会用到方式是有三种

  • 自动连接(AutoConnection),默认的连接方式。如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接。

  • 直接连接(DirectConnection)。当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行。

  • 队列连接(QueuedConnection)。当控制权回到接受者所在线程的事件循环时,槽函数被调用。这时候需要将信号的参数塞到信号队列里。槽函数在接受者所在线程执行。

所以在线程间进行信号槽连接时,使用的是队列连接方式。在项目中,我定义的信号和槽的参数是这样的:

signals:
//自定义发送的信号
void myThreadSignal(const int, string, string, string, string);

貌似没什么问题,然而实际运行起来槽函数根本就没有被调用,程序没有崩溃,VS也没报错。在查阅了N多博客和资料中才发现,在线程间进行信号槽连接时,参数不能随便写。

为什么呢?我的后四个参数是标准库中的string类型,这不是元对象系统内置的类型,也不是c++的基本类型,系统无法识别,然后就没有进入信号槽队列中了,自然就会出现问题。解决方法有三种,最简单的就是使用Qt的数据类型了

signals:
//自定义发送的信号
void myThreadSignal(const int, QString, QString, QString, QString);

第二种方法就是往元对象系统里注册这个类型。注意,在qRegisterMetaType函数被调用时,这个类型应该要确保已经被完好地定义了。

qRegisterMetaType<MyClass>("MyCl方法三是改变信号槽的连接方式,将默认的队列连接方式改为直接连接方式,这样的话信号的参数直接进入槽函数中被使用,槽函数立刻调用,不会进入信号槽队列中。但这种方式官方认为有风险,不建议使用。ss");

方法三是改变信号槽的连接方式,将默认的队列连接方式改为直接连接方式,这样的话信号的参数直接进入槽函数中被使用,槽函数立刻调用,不会进入信号槽队列中。但这种方式官方认为有风险,不建议使用。

connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::DirectConnection)

总结一下:

  • 一定要用信号槽机制,别想着直接调用,你会发现并没有在子线程中执行。

  • 自定义的类不能指定父对象,因为moveToThread函数会将线程对象指定为自定义的类的父对象,当自定义的类对象已经有了父对象,就会报错。

  • 当一个变量需要在多个线程间进行访问时,最好加上voliate关键字,以免读取到的是旧的值。当然,Qt中提供了线程同步的支持,比如互斥锁之类的玩意,使用这些方式来访问变量会更加安全。

QT从入门到入土(四)——多线程的更多相关文章

  1. QT从入门到入土(四)——多线程(QtConcurrent::run())

    引言 在前面对Qt多线程(QThread)做了详细的分析:QT从入门到入土(四)--多线程(QThread) - 唯有自己强大 - 博客园 (cnblogs.com) 但是最近在做项目时候,要将一个函 ...

  2. QT从入门到入土(三)——信号和槽机制

    摘要 信号槽是 Qt 框架引以为豪的机制之一.所谓信号槽,实际就是观察者模式.当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号 (signal).这种发出是没有目的的,类似广播 ...

  3. QT从入门到入土(二)——对象模型(对象树)和窗口坐标体系

    摘要 我们使用的标准 C++,其设计的对象模型虽然已经提供了非常高效的 RTTI 支持,但是在某些方面还是不够灵活.比如在 GUI 编程方面,既需要高效的运行效率也需要强大的灵活性,诸如删除某窗口时可 ...

  4. QT从入门到入土(一)——Qt5.14.2安装教程和VS2019环境配置

    引言 24岁的某天,承载着周围人的关心,一路南下.天晴心静,听着斑马,不免对未来有些彷徨.但是呢,人生总要走陌生的路,看陌生的风景,所幸可以听着不变的歌,关心自己的人就那么多.就像是对庸常生活的一次越 ...

  5. QT从入门到入土(三)——文件的读写操作

     引言 文件的读写是很多应用程序具有的功能,甚至某些应用程序就是围绕着某一种格式文件的处 理而开发的,所以文件读写是应用程序开发的一个基本功能. Qt 提供了两种读写纯文本文件的基本方法: 用 QFi ...

  6. QT从入门到入土(八)——项目打包和发布

    引言 新手上路可谓是困难重重,你永远不知道下一个困难会在什么时候出现,在完成了运动控制卡封装发布过程中可谓是举步维艰.因此记录一下qt5+vs2019的打包发布方法. 打包一般分为两步: 将编译后的e ...

  7. QT从入门到入土(九)——TCP/IP网络通信(以及文件传输)

    引言 TCP/IP通信(即SOCKET通信)是通过网线将服务器Server端和客户机Client端进行连接,在遵循ISO/OSI模型的四层层级构架的基础上通过TCP/IP协议建立的通讯.控制器可以设置 ...

  8. RocketMQ入门到入土(二)事务消息&顺序消息

    接上一篇:RocketMQ入门到入土(一)新手也能看懂的原理和实战! 一.事务消息的由来 1.案例 引用官方的购物案例: 小明购买一个100元的东西,账户扣款100元的同时需要保证在下游的积分系统给小 ...

  9. 转载自~浮云比翼:Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)

    Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)   介绍:什么是线程,线程的优点是什么 线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可 ...

随机推荐

  1. Python+Selenium学习笔记14 - python官网的tutorial - just() fill() format()

    repr(x).rjust(n)  左侧空格填充,右侧列对齐,str()和repr()是一种输出,也可不用,直接x.rjust() repr(x).ljust(n)  右侧空格填充,左侧列对齐 rep ...

  2. lms框架模块详解

    模块的定义 一般地,开发者如果想要在一个自定义的程序集(包)中注册相关的服务,或者在应用初始化或停止时执行一段自定义的代码,那么您可能需要将该程序集(包)定义为一个模块. lms框架存在两种类型的模块 ...

  3. SQL Server 50道查询训练题,学生Student表

    下面这个是题目所用到的数据库! 首先你需要在你的SQL Sever数据库中创建[TestDb]这个数据库,接下来下面这个代码.直接复制在数据库里运行就好了! 1 USE [TestDb] 2 GO 3 ...

  4. TensorRT 7.2.1开发初步

    TensorRT 7.2.1开发初步 TensorRT 7.2.1开发人员指南演示了如何使用C ++和Python API来实现最常见的深度学习层.它显示了如何采用深度学习框架构建现有模型,并使用该模 ...

  5. 第五周 Spring框架

    一.Spring框架设计 Spring framework 6大模块 1.1 Spring AOP AOP: 面向切面编程 Spring 早期版本的核心功能,管理对象声明周期和对象装配 为了实现管理和 ...

  6. 工作流中容器化的依赖注入!Activiti集成CDI实现工作流的可配置型和可扩展型

    Activiti工作流集成CDI简介 activiti-cdi模块提供activiti的可配置型和cdi扩展 activiti-cdi的特性: 支持 @BusinessProcessScoped be ...

  7. 包及权限配置&java存储机理绘制

    包及权限配置 包的声明和导入 //声明 package aa.bb.cc; public class A{;} class B{;} //即在java输出目录aa.bb.cc中放入编译后的A.clas ...

  8. 重新整理 mysql 基础篇————— 事务隔离级别[四]

    前言 简单介绍一下事务隔离的基本 正文 Read Uncommitted(未提交读) 这个就是读未提交.就是说在事务未提交的时候,其他事务也可以读取到未提交的数据. 这里举一个例子,还是前一篇的例子. ...

  9. [Linux]经典面试题 - 网络基础 - TCP三次握手

    [Linux]经典面试题 - 网络基础 - TCP三次握手 目录 [Linux]经典面试题 - 网络基础 - TCP三次握手 一.TCP报文格式 1.1 TCP报头 1.2 报文图例 二.TCP三次握 ...

  10. 6.6考试总结(NOIP模拟4)

    前言 考试这种东西暴力拉满就对了QAQ T1 随 题解 解题思路 DP+矩阵乘(快速幂)+数论 又是一道与期望无关的期望题,显然答案是 总情况/情况数(\(n^m\)). 接下来的问题就是对于总情况的 ...