【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()介绍

  • 该静态函数调用 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.界面搭建,红色注释为个控件对象名称

2.头文件如下

#ifndef QTCONCURRENTDEMO_H
#define QTCONCURRENTDEMO_H

#include <QWidget>
#include <QDebug>
#include <QtConcurrent>

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

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

void QtConcurrentDemo::working()
{
    qDebug()<<__FUNCTION__<<QThread::currentThreadId();
    int i=0;
    while (flag) {
        ui->label->setNum(i++);
        qDebug()<<__FUNCTION__<<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解锁不同的资源,那么两个线程将永远等待下去,因此应用程序将被冻结。

方便类:

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

2.高级事件队列

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

猜你喜欢

转载自blog.csdn.net/weixin_43729127/article/details/129049828
今日推荐