【QT专栏】QT中实现多线程的四种方式和线程同步

这篇具有很好参考价值的文章主要介绍了【QT专栏】QT中实现多线程的四种方式和线程同步。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一、继承QThread

1.基本概念

2.操作流程

二、继承QObject(推荐)

1.基本概念

2.操作流程

三、继承QRunnable,配合QThreadPool实现多线程

1.外界通信

2.QMetaObject::invokeMethod()介绍

3.QMetaObject::invokeMethod()使用方式

四、使用QtConcurrent::run()

1.基本概念

2.操作流程

3.实现案例

五,线程同步

1.低级同步原语

2.高级事件队列


一、继承QThread

1.基本概念

  • 一个QThread类的对象管理一个子线程,自定义一个类继承自QThread,并重写虚函数run(),在run()函数里实现线程需要完成的复杂操作(注意QThread只有run函数是在新线程里的)。
  • 一般在主线程创建工作子线程,并调用start(),开始执行工作子线程的任务。start()会在内部调用run()函数,进入工作线程的事件循环,在run()函数里调用exit()或quit()可以结束线程的事件循环,或者在工作主线程里调用terminate()强制结束线程。

2.操作流程

1.创建一个继承QThread线程类的子类,记得包含头文件QThread

class subThread : public QThread
{
    ...
};

 2.重写父类的虚函数run()方法,在该方法内部实现子线程需要完成的复杂业务

class subThread : public QThread
{
    ...
protected:
    void run(){
        //全部在这里处理子线程的复杂业务
    }
};

3.在主线程中创建子线程对象

subThread* st = new subThread;

4.启动子线程,调用start()方法

st->start();

二、继承QObject(推荐)

1.基本概念

  • 如果有编写多个业务类,各不相关的业务逻辑需要被处理,就可以选择这种方式,将业务逻辑放入对应业务类的公共函数中,然后将这些业务类的实例对象移动到对应的子线程中moveToThread()就可以了,这种编写多线程的方式比第一种更加灵活,可读性也更强,更易于维护

2.操作流程

1.创建一个继承QObject类的子类,记得包含头文件QObject

class subObject : public QObject
{
    ...
}

2.在该类中添加一个公共的成员函数,主要负责子线程中要执行的业务逻辑

class subObject : public QObject
{
    ...
public:
    void working();    //函数名称随意取,传入的参数根据实际需求添加
}

3.在主线程中创建一个QThread对象,也就是子线程的对象

QThread* subThread =  new QThread;

4.主线程中创建该类的对象(创建该类对象千万不要指定父对象)

subObject* subObj = new subObject(this);      //error
subObject* subObj = new subObject;            //OK

5.将工作类subObject对象移动到创建的子线程对象中,需要调用继承自QObject类提供的 moveToThread()方法

// void moveToThread(QThread *targetThread)
subObj->moveToThread(subThread);

6. 启动子线程,调用start()方法,但这时候并不会启动子线程的工作函数

subThread->start();

7.在主线程中通过信号槽调用线程类subObject对象的工作函数,这时候才会到子线程中运行该工作函数

connect(ui->pushButton,&QPushButton::clicked,subObj,&subObject::working);

参考文章 Qt中多线程的使用 | 爱编程的大丙

三、继承QRunnable,配合QThreadPool实现多线程

使用信号槽通信方式可以查看这篇文章 Qt中线程池的使用 | 爱编程的大丙

1.外界通信

  • QRunnable类是所有可运行对象的基类,没有继承于QObject,所以就不能使用信号槽功能与外界通信
  • 使用多继承,就是让线程类同时继承QObjectQRunnable(上述文章就使用这种方式,但不推荐,毕竟C#都是单继承的,不多解释啦),让该线程类能够支持信号槽的使用
  • 使用QMetaObject::invokeMethod()

2.QMetaObject::invokeMethod()介绍

qt多线程,Qt,开发语言,qt

  • 该静态函数调用 obj 对象上的成员如信号,槽名,Q_INVOKABLE声明的函数(能够被Qt元对象系统唤起,所以传入的第一个对象指针是QObject类型或QObject子类类型)。如果成员可以被调用,则返回true。如果没有这样的成员或形参不匹配,则返回false。
  • 调用可以是同步的,也可以是异步的,这取决于第三个参数类型ConnectionType:

Qt::DirectConnection
该成员将立即被调用,同步调用

Qt::QueuedConnection
一旦应用程序进入主事件循环,就会发送一个QEvent并调用成员,异步调用

Qt::BlockingQueuedConnection
该方法将以与Qt::QueuedConnection相同的方式被调用,除了当前线程将阻塞,直到事件被传递。使用这种连接类型在同一线程中的对象之间通信将导致死锁。

Qt::AutoConnection
如果obj与调用者位于同一个线程中,则同步调用成员;否则,它将异步调用成员。
  • 成员函数调用的返回值放在ret中,如果调用是异步的,则无法计算返回值。您最多可以向成员函数传递十个参数。
  • QGenericArgumentQGenericReturnArgument是内部的辅助类。因为信号和槽可以动态调用,所以必须使用Q_ARG()Q_RETURN_ARG()宏将参数括起来。Q_ARG()接受类型名和该类型的const引用,Q_RETURN_ARG()接受一个类型名和一个非const引用。

3.QMetaObject::invokeMethod()使用方式

1.线程类中通信的函数

Q_INVOKABLE void externalTonXin(QString info);

2.在重写虚函数run内加入该接口调用方式,参数m_obj为主线程对象指针,"externalTonXin"是要调用被Q_INVOKABLE声明的函数

QMetaObject::invokeMethod(m_obj,"externalTonXin",Q_ARG(QString,"子线程主线程通信..."));

3.在主线程创建线程对象时,需要将主界面对象(this)传入线程对象构造函数中

RunnableThread* st = new RunnableThread(this);

参考文章 Qt线程之QRunnable的使用详解

四、使用QtConcurrent::run()

1.基本概念

Qt Concurrent模块包含支持程序代码并发执行的功能,可以在不使用低级线程原语的情况下编写多线程程序,它是一个单独的模块,使用起来也非常简单,不用向上面三种那样去为了某个业务处理逻辑而编写线程类,这种方式只需要你把耗时的某个接口传入该接口就行,具有用法看下面

2.操作流程

1.在.pro文件中添加QT += concurrent

2.然后在耗时接口所在的头文件中包含QtConcurrent,并在所在源文件中调用QtConcurrent::run()

QtConcurrent::run(this,&QtConcurrentDemo::working);

3.实现案例

1.界面搭建,红色注释为个控件对象名称

qt多线程,Qt,开发语言,qt

2.头文件如下

#ifndef QTCONCURRENTDEMO_H
#define QTCONCURRENTDEMO_H

#include <QWidget>
#include <QtConcurrent>
#include <QDebug>
#define cout qDebug() << "[" << __FUNCTION__ << "][" << __LINE__ << "]:"

namespace Ui {
class QtConcurrentDemo;
}

class QtConcurrentDemo : public QWidget
{
    Q_OBJECT

public:
    explicit QtConcurrentDemo(QWidget *parent = nullptr);
    ~QtConcurrentDemo();

    void working();     //耗时接口,主线程中运行会阻塞,子线程中能正常运行

private:
    Ui::QtConcurrentDemo *ui;
    bool flag;  //控制线程开闭
};

#endif // QTCONCURRENTDEMO_H

3.源文件如下

#include "qtconcurrentdemo.h"
#include "ui_qtconcurrentdemo.h"

QtConcurrentDemo::QtConcurrentDemo(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::QtConcurrentDemo)
{
    ui->setupUi(this);

    flag = false;
    connect(ui->startThread,&QPushButton::clicked,this,[=](){
        //耗时接口 working 在子线程中运行
        cout<<QThread::currentThreadId();
        flag = true;
        QtConcurrent::run(this,&QtConcurrentDemo::working);
    });
    connect(ui->startNotThread,&QPushButton::clicked,this,[=](){
        //耗时接口 working 在主线程中运行
        cout<<QThread::currentThreadId();
        flag = true;
        working();
    });
    connect(ui->closeThread,&QPushButton::clicked,this,[=](){
        //关闭线程
        cout<<QThread::currentThreadId();
        flag = false;
        ui->label->setNum(0);
    });
}

QtConcurrentDemo::~QtConcurrentDemo()
{
    delete ui;
}

void QtConcurrentDemo::working()
{
    cout<<QThread::currentThreadId();
    int i=0;
    while (flag) {
        ui->label->setNum(i++);
        cout<<i;
        QThread::sleep(1);
    }
}

五,线程同步

虽然线程的目的是允许代码并行运行,但有时线程必须停止并等待其他线程。例如,如果两个线程试图同时写入同一个变量,结果是未定义的。强迫线程相互等待的原则称为互斥。这是保护数据等共享资源的常用技术。

Qt提供了用于同步线程的低级原语和高级机制,介绍如下:

1.低级同步原语

  1. QMutex 是强制互斥的基本类。线程锁定互斥锁是为了访问共享资源。如果第二个线程在互斥锁已经锁定时试图锁定互斥锁,则第二个线程将进入休眠状态,直到第一个线程完成其任务并解锁互斥锁。
  2. QReadWriteLock 类似于QMutex,除了它区分了“读”和“写”访问。当一段数据没有被写入时,多个线程同时从中读取是安全的。QMutex强制多个读取器轮流读取共享数据,而QReadWriteLock允许同时读取,从而提高了并行性。(读锁共享,写锁互斥
  3. QSemaphore 是QMutex的一个推广,它保护了一定数量的相同资源。相反,QMutex只保护一个资源。比如信号量的一个典型应用:同步生产者和消费者之间对循环缓冲区的访问。
  4. QWaitCondition 同步线程不是通过强制互斥,而是通过提供一个条件变量。其他原语使线程等待资源被解锁,而QWaitCondition使线程等待特定条件被满足。为了让等待的线程继续,可以调用wakeOne()随机唤醒一个线程,或者调用wakeAll()同时唤醒所有线程。

注意:Qt的这些同步类依赖于使用正确对齐的指针。例如,不能在MSVC中使用打包类。

这些同步类可用于使方法线程安全。然而,这样做会导致性能损失,这就是为什么大多数Qt方法不是线程安全的。

风险:

  • 如果一个线程锁定了一个资源,但没有解锁它,应用程序可能会冻结,因为该资源将对其他线程永久不可用。例如,如果抛出异常并强制当前函数在不释放锁的情况下返回,就会发生这种情况。
  • 另一个类似的场景是死锁。例如,假设线程A正在等待线程B解锁一个资源。如果线程B也在等待线程A解锁不同的资源,那么两个线程将永远等待下去,因此应用程序将被冻结。

方便类:文章来源地址https://www.toymoban.com/news/detail-828516.html

  • QMutexLocker,QReadLocker,QWriteLocker是方便类,它们使QMutex和QReadWriteLock的使用更容易。这些方便类都是在构造时锁定资源,在析构时自动解锁释放资源。它们被设计用来简化使用QMutex和QReadWriteLock的代码,从而降低资源被意外永久锁定的几率。

2.高级事件队列

  1. Qt的事件系统对于线程间通信非常有用。每个线程都有自己的事件循环。要调用另一个线程中的槽(或任何可调用方法),请将该调用放在目标线程的事件循环中。这让目标线程在槽开始运行之前完成当前任务,而原始线程继续并行运行。
  2. 若要在事件循环中放置调用,请建立排队的信号槽连接。每当信号发出时,它的参数将被事件系统记录,信号接收器所在的线程将运行该插槽。或者调用QMetaObject::invokeMethod()在没有信号的情况下也能达到同样的效果。在这两种情况下,都必须使用排队连接,因为直接连接绕过事件系统并在当前线程中立即运行该方法。
  3. 与使用低级原语不同,使用事件系统进行线程同步时没有死锁的风险。但是,事件系统并不强制互斥。如果可调用方法访问共享数据,它们仍然必须受到低级原语的保护。
  4. 尽管如此,Qt的事件系统以及隐式共享数据结构为传统的线程锁定提供了一种替代方案。如果信号和槽是专用的,并且线程之间没有共享变量,那么多线程程序完全可以不使用低级原语。

到了这里,关于【QT专栏】QT中实现多线程的四种方式和线程同步的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • View 的四种 OnClick 方式

    嗨喽,大家好!今天呢,我跟大家聊一聊Android 的View 的点击事件onClick 。额,有点拗口(^_^) 。 看过我的文章的人可能会好奇,你怎么写Android的文章了啊?说起这啊,就是我的血泪史了,此处省略一万字.................... 废话不多说,让我们代码走起,风里来,雨里去,唯有代

    2023年04月15日
    浏览(42)
  • CSS中的四种定位方式

    在CSS中定位有以下4种: 静态定位 - static 相对定位 - relative 绝对定位 - absolute 固定定位 - fixed 静态定位是css中的默认定位方式,也就是没有定位。在此定位方式中设置:top,bottom,left,right,z-index 这些属性都是无效的。 相对位置前的位置: 相对位置后的位置: 可以看到该

    2024年02月08日
    浏览(85)
  • JavaScript中的四种枚举方式

    字符串和数字具有无数个值,而其他类型如布尔值则是有限的集合。 一周的日子(星期一,星期二,...,星期日),一年的季节(冬季,春季,夏季,秋季)和基本方向(北,东,南,西)都是具有有限值集合的例子。 当一个变量有一个来自有限的预定义常量的值时,使用

    2024年02月03日
    浏览(56)
  • STM32的四种开发方式

    首先看下ST官方给出的四种开发方式的比较 寄存器开发 寄存器编程对于从51等等芯片过渡过来的小伙伴并不陌生,不管你是什么库,最终操作的还是寄存器,所以对于标准库、HAL库、LL库都是在寄存器上的编程,所以可以直接在各种库中直接操作寄存器。 但寄存器开发方法到

    2024年02月11日
    浏览(41)
  • 单例模式的四种创建方式

    单例模式是日常开发中最常见的一种设计模式,常用来做为池对象,或者计数器之类的需要保证全局唯一的场景。 单例模式的目的是保证在整个程序中只存在一个对象实例,使用单例一个前提条件就是构造器私有化,不允许通过new 对象的方式。单例模式的实现主要方式有如

    2024年02月01日
    浏览(43)
  • gRpc的四种通信方式详细介绍

    🌷🍁 博主猫头虎 带您 Go to New World.✨🍁 🦄 博客首页——猫头虎的博客🎐 🐳《面试题大全专栏》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍专栏》学会IDEA常用操作,工作效率翻倍~💐 🌊 《100天精通Golang(基础入门篇)》学会Golang语言

    2024年02月11日
    浏览(40)
  • C++文件读取的四种方式

    C++可以根据不同的目的来选取文件的读取方式,目前为止学习了C++中的四种文件读取方式。 C++文件读取的一般步骤: 1、包含头文件 #includefstream 2、创建流对象:ifstream ifs (这里的ifs是自己起的流对象名字) 3、打开文件:file.open(\\\"文件路径\\\",\\\"打开方式\\\"),打开文件后并判断文件是

    2024年02月11日
    浏览(40)
  • SpringBoot导出Excel的四种方式

           近期接到了一个小需求,要将系统中的数据导出为Excel,且能将Excel数据导入到系统。对于大多数研发人员来说,这算是一个最基本的操作了。但是……我居然有点方!         好多年没有实操这种基础的功能了。我对于excel导入导出的印象还停留在才入行时的工作经

    2024年02月03日
    浏览(41)
  • C#对象的四种比较方式

    1.ReferenceEquals(object o1, object o2): 静态方法: 比较两个对象的引用,引用相同返回true,否则返回false,同为null是返回true; ReferenceEquals进行值类型比较时总是返回false,因为两个值类型需要分别装箱到对象中,是不同的引用 ; 从名称中便可知它用来比较两者是否是相同的引

    2024年02月16日
    浏览(32)
  • Java创建数组的四种方式

    1.使用默认值来初始化 语法: 数组元素类型 [] 数组名称 = new 数组元素类型 [数组长度] EG: int [] nums = new int [5]; //创建了一个类型为int,名字为nums ,长度为5的数组 2.先声明一个数组,再给值 语法: 数据元素类型 [] 数组名称; 数组名称 = new 数组元素类型[数组长度]; EG: int [] nums; num

    2024年02月09日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包