• Qt常用的多线程使用方式


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

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

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

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

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

            

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

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

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

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

    1. #include
    2. class Thread: public QThread
    3. {
    4. public:
    5. Thread(QObject* parent=nullptr);
    6. signals:
    7. void signalNotify();
    8. public slots:
    9. void receiveMesg();
    10. protected:
    11. void run()
    12. {
    13. //do something
    14. }
    15. }
    16. /// widget.cpp ///
    17. #include "Thread.h"
    18. #include
    19. Widget::Widget(QWidget* parent)
    20. {
    21. Thread *t = new Thread();
    22. t->start();
    23. }

            这是最基础的用法和结构,派生于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?一方面源代码的实现中要求不能这么做,如下:       

    1. void QObject::moveToThread(QThread *targetThread)
    2. {
    3. Q_D(QObject);
    4. if (d->threadData->thread.loadAcquire() == targetThread) {
    5. // object is already in this thread
    6. return;
    7. }
    8. if (d->parent != 0) {
    9. qWarning("QObject::moveToThread: Cannot move objects with a parent");
    10. return;
    11. }
    12. if (d->isWidget) {
    13. qWarning("QObject::moveToThread: Widgets cannot be moved to a new thread");
    14. return;
    15. }
    16. //do something else.....
    17. }

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

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

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

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

    1. #include
    2. class Work: public QObject
    3. {
    4. Q_OBJECT
    5. public:
    6. Work(QObject* parent=nullptr);
    7. signals:
    8. void sigSendMsg(const QString&);
    9. public slots:
    10. void receiveMsg(const QString& msg);
    11. }
    12. Widget::Widget(QWidget* parent)
    13. {
    14. mWork = new Work;
    15. connect(mWork, &Work::sigSendMsg, this, &Widget::slotFunc);
    16. workThread = new QThread;
    17. connect(workThread, &QThread::finish, mWork, &Work::deleteLater);
    18. connect(workThread, &QThread::finish, workThread, &QThread::deleteLater);
    19. mWork->moveToThread(workThread)
    20. workThread->start();
    21. }
    22. Widget::~Widget()
    23. {
    24. workThread->quit();
    25. workThread->wait();
    26. }

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

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

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

            直接上demo:       

    1. class HelloWorldTask : public QRunnable
    2. {
    3. void run() override
    4. {
    5. qDebug() << "Hello world from thread" << QThread::currentThread();
    6. }
    7. };
    8. HelloWorldTask *hello = new HelloWorldTask();
    9. // QThreadPool takes ownership and deletes 'hello' automatically
    10. QThreadPool::globalInstance()->start(hello);

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

    开启多少个线程合适呢?

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

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

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

    QtConcurrent::run();

            

    拓展内容:

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

    1. #include
    2. #ifdef Q_OS_LINUX
    3. #include
    4. #endif
    5. #ifdef Q_OS_WIN
    6. #include
    7. #endif
    8. int main()
    9. {
    10. #ifdef Q_OS_LINUX
    11. qDebug()<<pthread_self();
    12. #endif
    13. #ifdef Q_OS_WIN
    14. qDebug()<<GetCurrentThreadId();
    15. #endif
    16. }

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

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

    1. while(true)
    2. {
    3. if(!isInterruptionRequested())
    4. {
    5. //耗时操作
    6. }
    7. }

    写在最后:

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

  • 相关阅读:
    promise函数
    利用 SOAR 加快事件响应并加强网络安全
    mysql基于Python的影院会员管理系统的设计与实现毕业设计源码131621
    聊聊 C++ 中的几种智能指针 (下)
    输电线路故障诊断(Python代码,逻辑回归、决策树、随机森林、XGBoost和支持向量机五种不同方法诊断)
    Redis之淘汰策略
    【面试:并发篇39:多线程:线程池】ThreadPoolExecutor类-提交、停止
    HashMap源码解析
    前端进击笔记第二十四节 如何进行技术方案调研与设计?
    国外视频搬运素材去哪里找?可搬运下载国外的素材网站库分享
  • 原文地址:https://blog.csdn.net/weixin_43935710/article/details/136370326