Qt常用的多线程使用方式

目前(Qt5)常用的多线程的方式?

1、派生于QThread然后重写run()函数

2、通过将派生QObject的类对象通过moveToThread()来移动到新的线程中

3、通过inherit QRunnable类然后重写run()方法、然后借助QThreadPool线程池来实现多线程

4、通过高级语法 QtConcurrent模块来实现多线程

本文主要讲解不同多线程的使用方式,并穿插不同之处和注意事项,方便后来人的学习

在开始之前,我们需要先明确几个概念:对象和线程。对象指的是派生于QObject以及QThread类的实例化对象,线程指的是多线程对象开辟出的新线程,这个线程和主线程是两个并行存在。希望不要将对象和线程搞混了。

一、派生于QThread然后重写run()函数

这种方式是使用比较传统的方式,直接上一个简单的demo:

cpp 复制代码
#include <QThread>

class Thread: public QThread
{
public:
    Thread(QObject* parent=nullptr);
    
signals:
    void signalNotify();

public slots:
    void receiveMesg();

protected:
    void run()
    {
        //do something
    }

}

///   widget.cpp  ///
#include "Thread.h"
#include <QApplication>
Widget::Widget(QWidget* parent)
{
    Thread *t = new Thread();
    t->start(); 
}

这是最基础的用法和结构,派生于QThread、重写run()函数、创建线程对象以及开启线程。在这种方式下,耗时操作都仍给了run()函数,所以如果需求复杂一些,就需要在run()中实现具体的业务。但是有几点我们需要注意:

1)不要在多线程中直接操作UI

2)正确管理、使用定时器等资源

既然不能在run中直接操作UI,那我们要是想把逻辑运算的结果通知到UI又该怎么操作呢?通过信号槽的方式。这里要注意的是:在主线程中创建线程对象后,比如上面Thread实例化对象,这个线程对象是属于主线程的,所以在主线程中使用信号曹将线程对象和GUI对象连接起来后,这并不是多线程通信,真正子线程部分的是在run()中的逻辑

此外,如果想在子线程中使用定时器,一定要在run()中创建,停止也要在子线程中操作,切莫跨线程操作定时器。而且,在run()中创建的资源都是属于子线程的,对这些资源的操作一定要注意。在run()中连接的信号槽也是属于子线程的。

那想要在QThread中使用信号槽,仅在run()中绑定信号槽就行了吗?

不是,必须在run()中通过调用exec()来开启事务循环。只有开启事务循环,那么依赖于事务循环的种种特性:定时器、信号槽、TCP通信、网络请求以及各种QEvent等才能使用,明白了吧?要是用不到上面那些特性,只想执行一些耗时操作,那么能不能不加exec()?要是不加的话,子线程在运行完耗时逻辑后就会结束线程。

在代码中看到在实例化Thread对象指针的时候没有指定parent,那能指定parent吗?不能,为什么创建QThread派生类对象时候不能指定parent?一方面源代码的实现中要求不能这么做,如下:

cpp 复制代码
void QObject::moveToThread(QThread *targetThread)
{
    Q_D(QObject);

    if (d->threadData->thread.loadAcquire() == targetThread) {
        // object is already in this thread
        return;
    }

    if (d->parent != 0) {
        qWarning("QObject::moveToThread: Cannot move objects with a parent");
        return;
    }
    if (d->isWidget) {
        qWarning("QObject::moveToThread: Widgets cannot be moved to a new thread");
        return;
    }
    
    //do something else.....
}

还有就是存在潜在的风险,如果指定了parent,那么一旦parent生命周期结束了,那势必要回收parent占用的资源,这里面包括QThreadChild对象占用的资源。但是此时子线程很可能正在干活,人家正在吃饭呢,你把桌子掀了,我想乌鸦也不会答应吧?

既然没有指定parent就不能借助Qt的半自动内存回收机制,那就需要人为的手动删除内存,即通过QThread::finish信号来连接QThreadChild::deleteLater函数来实现对象资源的释放

二、通过将派生QObject的类对象通过moveToThread()来移动到新的线程中

这种方式适合于业务比较明确划分的情况,通过将一类业务单独抽离成一个类,然后将类的业务响应在多线程中执行。下面先上一个demo:

cpp 复制代码
#include <QObject>


class Work: public QObject
{
    Q_OBJECT

public:
    Work(QObject* parent=nullptr);

signals:
    void sigSendMsg(const QString&);

public slots:
    void receiveMsg(const QString& msg);
}


Widget::Widget(QWidget* parent)
{
    mWork = new Work;
    connect(mWork, &Work::sigSendMsg, this, &Widget::slotFunc);

    
    workThread = new QThread;    
    connect(workThread, &QThread::finish, mWork, &Work::deleteLater);
    connect(workThread, &QThread::finish, workThread, &QThread::deleteLater);
    mWork->moveToThread(workThread)
    workThread->start();
}

Widget::~Widget()
{
    workThread->quit();
    workThread->wait();
}

这种方式的核心就是moveToThread(), moveToThread移动了什么?是线程对象的归属权吗?No!是将线程对象中的槽函数放在了新线程中执行,而线程对象依然属于创建它所在的线程中。切莫以为执行了moveToThread后线程对象所有的一切都打包给新线程了。在哪创建就属于哪,在多线程中依然适用。使用moveToThread方式时、对象不能设置parent,不然无法完成移动。

既然moveToThread也是借助于QThread,那么如果此时有一个inherit QThread的子类ThreadA,以及通过move方式到ThreadA中的线程B,那这两个线程run()和线程B谁先执行呢?通过测试发现,run()先执行,执行完run()后再执行move进来的槽函数。

三、通过inherit QRunnable类然后重写run()方法、然后借助QThreadPool线程池来实现多线程

直接上demo:

cpp 复制代码
  class HelloWorldTask : public QRunnable
  {
      void run() override
      {
          qDebug() << "Hello world from thread" << QThread::currentThread();
      }
  };

  HelloWorldTask *hello = new HelloWorldTask();
  // QThreadPool takes ownership and deletes 'hello' automatically
  QThreadPool::globalInstance()->start(hello);

这种方式的核心并不是如何使用,而是了解线程池 。线程池里有多少个正在干活的线程activeThreadCount?这个池子又能放下多少个线程maxThreadCount?要是现在没有多余的线程能够用、那么被丢进池子里的多线程任务又是如何处理的?看QThreadPool了解。

开启多少个线程合适呢?

线程的开辟和切换需要消耗CPU资源的,尤其是涉及到CPU的上下文切换,所以并不是线程开的越多越好,那多少是理想值呢?一般根据业务要求来,有的是内核数量的4倍,有的高达16倍。根据QThreadPool::maxThreadCount()来看,这个于计算机的real and logic cores数量相关

四、通过高级语法 QtConcurrent模块来实现多线程

这种方式就很简单了,适用于做一些纯属于简单的累活,干完就拉到,中间不需要交互过程,一般都是配合lambda表达式使用,用的时候别忘了在.pro中添加QT += concurrent

cpp 复制代码
QtConcurrent::run();

拓展内容:

关于currentThreadId(),正确获取多线程id的方式:

cpp 复制代码
#include <QCoreApplication>
#ifdef Q_OS_LINUX
    #include <pthread.h>
#endif

#ifdef Q_OS_WIN
    #include <windows.h>
#endif

int main()
{
    #ifdef Q_OS_LINUX
        qDebug()<<pthread_self();
    #endif

    #ifdef Q_OS_WIN
        qDebug()<<GetCurrentThreadId();
    #endif
}

多线程也是有优先级,可以通过QThread::setPriority()来设置

此外,还提供了QThread::isInterruptionRequested()来判断是否可以提前跳出线程循环:

cpp 复制代码
while(true)
{
    if(!isInterruptionRequested())
    {
        //耗时操作
    }
}

写在最后:

上面介绍了常用的多线程方式,那实际的工作中还有一个技巧,就是不通过信号槽的方式在主线程中仍然能调用子线程函数的方式:QMetaObject::invokeMethod,参数可以指定是跨线程调用还是直接在同线程中调用

相关推荐
A.A呐7 小时前
【QT第三章】常用控件2
开发语言·qt
笨笨马甲8 小时前
Qt 实现三维坐标系的方法
开发语言·qt
谁动了我的代码?8 小时前
VNC中使用QT的GDB调试,触发断点时与界面窗口交互导致整个VNC冻结
开发语言·qt·svn
肖恭伟9 小时前
QtCreator Linux ubuntu24.04问题集合
linux·windows·qt
vegetablesssss10 小时前
QT国际化翻译
qt
困死,根本不会10 小时前
Qt Designer 基础操作学习笔记
开发语言·笔记·qt·学习·microsoft
喜欢喝果茶.11 小时前
Qt MQTT部署
开发语言·qt
浅碎时光80711 小时前
Qt 窗口 (菜单 工具栏 状态栏 浮动窗口 对话框)
qt
GIS阵地11 小时前
一场由Qt5 painter的drawRect引起的血雨腥风
开发语言·qt·gis·qgis
娇娇yyyyyy11 小时前
QT编程(8): qt自定义菜单项
qt·microsoft