• Qt应用开发(进阶篇)——线程 QThread


    一、前言

            QThread类继承QObject基类,是Qt经典基础工具类,QThread类提供了一种独立于平台的方式来管理线程,让开发者能够快速的完成多线程的创建和使用。

            正常情况下,一个PC程序使用到多线程的概率是非常高的,在不同方式的通讯场景使用、在耗时任务中使用、在独立的任务中使用等等。所以学习好多线程的使用是非常重要的,这也是程序员必备的技能之一。在C++中也有线程的功能,但是Qt提供的QThread线程,更适用于在Qt框架中使用。

            QThread对象管理一个独立的线程,调用start()启用,启用成功触发started()信号,当线程结束的时候触发finished()信号,并提供isFinished()、isRunning()查询状态。使用exit()quit()主动退出线程,wait()阻塞等待线程结束。

            QThread线程还可以使用setPriority()设置优先级,而优先级参数的效果取决于操作系统的调度策略,在一些不支持线程优先的操作系统,比如Linux,优先级将被忽略。

    二、创建线程方法一

            QThreads在run()中开始执行,默认情况下,run()通过调用exec()启动事件循环,并在线程内运行Qt事件循环,当run函数执行完成的时候,线程执行结束触发信号并退出。

            所以创建线程的第一种方式就是继承QThread,重新实现run函数。

    1. #ifndef MYTHREAD_H
    2. #define MYTHREAD_H
    3. #include
    4. #include
    5. class MyThread : public QThread
    6. {
    7. Q_OBJECT
    8. public:
    9. explicit MyThread(QObject *parent = nullptr);
    10. ~MyThread();
    11. protected:
    12. virtual void run();
    13. Q_SIGNALS:
    14. void printMsg(int);
    15. };
    16. #endif // MYTHREAD_H
    1. #include "mythread.h"
    2. #include
    3. MyThread::MyThread(QObject *parent) : QThread(parent)
    4. {
    5. }
    6. MyThread::~MyThread()
    7. {
    8. qDebug()<<"~MyThread ";
    9. }
    10. void MyThread::run()
    11. {
    12. qDebug()<<"MyThread "<<this->currentThreadId();
    13. for(int i = 0 ; i < 5 ;i++)
    14. {
    15. qDebug()<
    16. emit printMsg(i*2);
    17. }
    18. }
    1. qDebug()<<"main thread id"<currentThreadId();
    2. thread = new MyThread(this);
    3. thread->start();
    4. connect(thread,&MyThread::finished,this,[](){
    5. qDebug()<<"thread finish";
    6. });
    7. connect(thread,&MyThread::printMsg,this,[](int num){
    8. qDebug()<<"thread Msg "<
    9. });

            在上面的例子中,我们继承QThread,重新实现Run函数,在线程中打印数字,并且抛出自定义的信号printMsg,主线程绑定信号打印信息。线程结束后QThread会触发finished信号,我们也绑定该信号打印信息,当主程序结束后,线程类调用析构函数并打印信息。

    1. main thread id 0x7ffb77158040
    2. MyThread 0x7ffb47767700
    3. 0
    4. 1
    5. 2
    6. 3
    7. 4
    8. thread Msg 0
    9. thread Msg 2
    10. thread Msg 4
    11. thread Msg 6
    12. thread Msg 8
    13. thread finish
    14. ~MyThread

            但是打印的结果并不是我们想要的,因为我们打印数字的同时一边在抛出信号,为什么会打印结束了,才开始执行槽函数呢,并没有两边交替打印,这是因为信号槽的连接方式默认为Qt::AutoConnection,而如果是线程之间的连接,会自动转换成Qt::QueuedConnection(当控制返回到接收者线程的事件循环时调用该槽,槽在接收者的线程中执行)。所以我们需要修改连接方式。

    1. connect(thread,&MyThread::printMsg,this,[](int num){
    2. qDebug()<<"thread Msg "<
    3. },Qt::DirectConnection);
    1. main thread id 0x7f2e6825d040
    2. MyThread 0x7f2e38a32700
    3. 0
    4. thread Msg 0
    5. 1
    6. thread Msg 2
    7. 2
    8. thread Msg 4
    9. 3
    10. thread Msg 6
    11. 4
    12. thread Msg 8
    13. thread finish
    14. ~MyThread

    二、创建线程方法二

            使用QObject::moveToThread()将工作对象移动到线程中,是Qt线程使用的第二种方式。

    1. #ifndef WORKER_H
    2. #define WORKER_H
    3. #include
    4. #include
    5. #include
    6. class worker : public QObject
    7. {
    8. Q_OBJECT
    9. public:
    10. explicit worker(QObject *parent = nullptr);
    11. Q_SIGNALS:
    12. void resultReady(QString result);
    13. public Q_SLOTS:
    14. void doWork(QString parameter);
    15. };
    16. #endif // WORKER_H
    1. #include "worker.h"
    2. worker::worker(QObject *parent) : QObject(parent)
    3. {
    4. }
    5. void worker::doWork(QString parameter) {
    6. qDebug()<currentThreadId();
    7. QString result;
    8. qDebug()<<"work doWork"<
    9. emit resultReady(result);
    10. }

            定义完工作类,我们要定义一个线程,并使用moveToThread把工作类“移动”到线程中。

    1. class MainWindow : public QMainWindow
    2. {
    3. Q_OBJECT
    4. public:
    5. MainWindow(QWidget *parent = nullptr);
    6. ~MainWindow();
    7. Q_SIGNALS:
    8. void doWork(QString);
    9. private slots:
    10. void on_pushButton_clicked();
    11. private:
    12. QThread *m_thread;
    13. worker *m_work;
    14. };
    1. MainWindow::MainWindow(QWidget *parent)
    2. : QMainWindow(parent)
    3. , ui(new Ui::MainWindow)
    4. {
    5. ui->setupUi(this);
    6. m_thread = new QThread(this);
    7. m_work = new worker();
    8. m_work->moveToThread(m_thread);
    9. connect(this,&MainWindow::doWork,m_work,&worker::doWork);
    10. connect(m_work,&worker::resultReady,this,[&](QString result){
    11. qDebug()<<"recv m_work Msg "<
    12. });
    13. m_thread->start();
    14. }
    1. void MainWindow::on_pushButton_clicked()
    2. {
    3. qDebug()<<"main thread id"<currentThreadId();
    4. emit doWork("hello world");
    5. }

             执行结果:

    1. main thread id 0x7f016253c040
    2. 0x7f013bfff700
    3. work doWork "hello world"
    4. recv m_work Msg ""

    三、线程使用注意事项

    1、CPU飙升

            初次使用QThread的同学,经常会出现一个问题,在线程执行的函数体中,使用死循环一直读串口或者后台等一些数据信息,读到信息抛出信号,读不到一直调用自定义读取的函数。在这种工况下,如果读取的函数是非阻塞的,那么整个CPU的资源都被子线程占用着,系统没办法合理的分配时间片。所以如果有需要使用读取线程并且用死循环读取信息的同学,一定要确保读取的函数是阻塞的或者做sleep操作。

    2、阻塞线程退出异常

            正常情况下,读取线程我们会定义一个变量,用来控制死循环的结束兼线程的结束。

    1. bool m_loop;
    2. void MyThread::run()
    3. {
    4. qDebug()<<"MyThread "<<this->currentThreadId();
    5. m_loop = true;
    6. while (m_loop) {
    7. qDebug()<<10;
    8. sleep(1);
    9. }
    10. }
    11. void MyThread::stop()
    12. {
    13. m_loop = false;
    14. }
    thread->stop();

            在上面的实例中,我们使用m_loop变量来结束死循环,让run函数结束,从而控制线程的结束。

            但是如果是阻塞的场景下,这样的逻辑就不够用了。直接退出会报“QThread: Destroyed while thread is still running”。

            因为死循环中,读取函数一直卡着,这样就没办法退出,这时候我们需要注意阻塞的退出方法,比如waitForReadyRead默认为3秒、linux中的select设置超时、socketcan使用阻塞则用关闭退出等等。配合使用QThread的wait函数,会阻塞在该函数等待线程的退出,让主线程退出子线程的时候做出等待的操作。

    1. thread->stop();
    2. thread->quit();
    3. thread->wait();

    3、moveToThread方式的线程退出

            使用此方式进行创建线程,它不像重写run一样,run函数结束线程就自动退出。我们一开始调用的是start,在程序中它一直都处于运行的状态,只是如果你没有使用信号触发,它会处于休眠状态。所以在程序结束的时候记得使用quit和wait退出线程,是否会报“QThread: Destroyed while thread is still running”。

    1. thread->quit();
    2. thread->wait();

    4、moveToThread方式的工作类不能有父类

            在第二例子中,在定义worker类的时候,不能写成:

    m_work = new worker(this);

            否则会有告警:

    QObject::moveToThread: Cannot move objects with a parent

            并且运行之后发现,work的函数调用其实是在主线程中,并没有在子线程中执行。

    1. main thread id 0x7f5299998040
    2. 0x7f5299998040
    3. work doWork "hello world"
    4. recv m_work Msg ""

    5、线程中使用成员类异常告警

            问题代码如下:

    1. #ifndef MYTHREAD_H
    2. #define MYTHREAD_H
    3. #include
    4. #include
    5. #include
    6. class MyThread : public QThread
    7. {
    8. Q_OBJECT
    9. public:
    10. explicit MyThread(QObject *parent = nullptr);
    11. ~MyThread();
    12. void stop();
    13. protected:
    14. virtual void run();
    15. Q_SIGNALS:
    16. void printMsg(int);
    17. private:
    18. QTimer *m_timer;
    19. bool m_loop;
    20. };
    21. #endif // MYTHREAD_H
    1. #include "mythread.h"
    2. #include
    3. MyThread::MyThread(QObject *parent) : QThread(parent)
    4. {
    5. m_timer = new QTimer(this);
    6. connect(m_timer,&QTimer::timeout,this,[]()
    7. {
    8. qDebug()<<"time out";
    9. });
    10. }
    11. MyThread::~MyThread()
    12. {
    13. qDebug()<<"~MyThread ";
    14. }
    15. void MyThread::run()
    16. {
    17. qDebug()<<"MyThread "<<this->currentThreadId();
    18. m_loop = true;
    19. m_timer->start(1000);
    20. while (m_loop) {
    21. qDebug()<<10;
    22. sleep(1);
    23. }
    24. }
    25. void MyThread::stop()
    26. {
    27. m_loop = false;
    28. }

            在上面的例子中,MyThread成员类变量QTimer,在构造函数中实例化,在run函数中启动,这时候线程启动的时候会报异常:

    QObject::startTimer: Timers cannot be started from another thread

            这是因为在重写run的这种方式中,除了run函数内其他函数包括类都是属于主线程的,包括构造函数。所以定时器是属于主线程的类,在子线程中控制它,就会告警,于是我们修改定时器定义:

    1. void MyThread::run()
    2. {
    3. qDebug()<<"MyThread "<<this->currentThreadId();
    4. m_loop = true;
    5. m_timer = new QTimer(this);
    6. connect(m_timer,&QTimer::timeout,this,[]()
    7. {
    8. qDebug()<<"time out";
    9. });
    10. m_timer->start(1000);
    11. while (m_loop) {
    12. qDebug()<<10;
    13. sleep(1);
    14. }
    15. }

            这时候又会报另一个错误,不能在子线程中为主线程的类创建子类。

    1. QObject: Cannot create children for a parent that is in a different thread.
    2. (Parent is MyThread(0x561d014478b0), parent's thread is QThread(0x561d01438e20), current thread is MyThread(0x561d014478b0)

            所以我们需要去掉this指针,就不会有这个错误,这时候需要注意指针的释放,因为我们没有定义父类,它不会跟随父类的释放而释放。

    1. void MyThread::run()
    2. {
    3. qDebug()<<"MyThread "<<this->currentThreadId();
    4. m_loop = true;
    5. QTimer *m_timer = new QTimer();
    6. connect(m_timer,&QTimer::timeout,this,[]()
    7. {
    8. qDebug()<<"time out";
    9. });
    10. m_timer->start(1000);
    11. while (m_loop) {
    12. qDebug()<<10;
    13. sleep(1);
    14. }
    15. }
  • 相关阅读:
    云原生之容器化:Docker三剑客之Docker Swarm
    一种基于Redis时间和权重关联的分布式优先级队列方法
    第一章:初识MySQL
    Vue基础语法(插值、指令、过滤器、计算属性与监听属性)
    Elasticsearch教程10】Mapping字段类型之数字Numbers
    Kubernetes(k8s)的Pod控制器DaemonSet详细讲解
    浅谈弧光保护在中低压电力系统中的重要性
    硅谷来信:Google、Facebook员工的“成长型思维”
    微信小程序根据开发环境切换域名
    Java开发岗位职责【杭州多测师】【杭州多测师_王sir】
  • 原文地址:https://blog.csdn.net/u014491932/article/details/134518494