引言

前面几篇已经对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. 3D点云几何拟合

    3D点云几何拟合 Supervised Fitting of Geometric Primitives to 3D Point Clouds 论文地址: http://openaccess.thecv ...

  2. SLAM架构的两篇顶会论文解析

    SLAM架构的两篇顶会论文解析 一. 基于superpoint的词袋和图验证的鲁棒闭环检测 标题:Robust Loop Closure Detection Based on Bag of Super ...

  3. 构建可扩展的GPU加速应用程序(NVIDIA HPC)

    构建可扩展的GPU加速应用程序(NVIDIA HPC) 研究人员.科学家和开发人员正在通过加速NVIDIA GPU上的高性能计算(HPC)应用来推进科学发展,NVIDIA GPU具有处理当今最具挑战性 ...

  4. 整理AI性能指标

    整理AI性能指标 Sorting out AI performance metrics 推理性能的最佳衡量标准是什么? 在人工智能加速器的世界里,对于给定的算法,芯片的性能经常以每秒万亿次的运算量(T ...

  5. MapReduce —— MapTask阶段源码分析(Input环节)

    不得不说阅读源码的过程,极其痛苦 .Dream Car 镇楼 ~ ! 虽说整个MapReduce过程也就只有Map阶段和Reduce阶段,但是仔细想想,在Map阶段要做哪些事情?这一阶段具体应该包含数 ...

  6. P5960 【模板】差分约束算法

    题目描述 给出一组包含 $m$ 个不等式,有 $n$ 个未知数的形如: 的不等式组,求任意一组满足这个不等式组的解. 输入格式 第一行为两个正整数 $n,m$,代表未知数的数量和不等式的数量. 接下来 ...

  7. 【NX二次开发】Block UI 线宽

    属性说明 常规         类型 描述     BlockID     String 控件ID     Enable     Logical 是否可操作     Group     Logical ...

  8. 谷歌:python速成课程笔记

    1.从用户那里获取信息 name = "Alex" print("hello" + name) 2.让python成为你的计算器 1 print(4+5) 2 ...

  9. 学习响应式编程 Reactor (5) - reactor 转换类操作符(2)

    Reactor 操作符 上篇文章我们将 Flux 和 Mono 的操作符分了 11 类,我们来继续学习转换类操作符的第 2 篇. 转换类操作符 转换类的操作符数量最多,平常过程中也是使用最频繁的. F ...

  10. 【dp】背包问题

    01背包 呐,为什么叫它01背包呢,因为装进去就是1,不装进去就是0.所以针对每个物品就两种状态,装,不装(请允许我用这么老套的开篇,相信听过很多次背包讲解的人,大多都是这个开篇的)所以咯,我这个背包 ...