• Qt的多线程编程


    Qt线程

    基本概念

    并发

    当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其他线程处于挂起状态。虽然看起来所有线程都是一起执行的,但是其实每个时间只有一个线程在执行,这种方式我们成为并发。

    并行

    当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行。因此,多核CPU可以同时执行多个进程。

    为什么需要使用线程

    例:给定需求计算一个复杂数据处理所花费的时间。我们有一个start按钮,和一个LCD显示时间,点击按钮开始计时并进行数据处理。

    我们创建一个定时器。在start的槽函数中启动定时器,并进行数据处理,数据处理利用QThread::sleep(5)来模拟。定时器每次触发timeout信号,更新LCD数字。代码如下:

    //widget.h
    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include 
    #include 
    #include 
    
    //利用定时器计算复杂计算花费的时间
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = 0);
        ~Widget();
    
    private slots:
        void on_pushButton_clicked();
    
    private:
        Ui::Widget *ui;
        QTimer *_timer = new QTimer(this);
    };
    
    #endif // WIDGET_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    //widget.cpp
    #include "widget.h"
    #include "ui_widget.h"
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget) {
        ui->setupUi(this);
    
        //只要定时器启动,自动触发timeout信号,每次触发timeout信号更新LCD显示
        connect(_timer, &QTimer::timeout, [=]() {
           static int i = 0;
           i++;
           ui->lcdNumber->display(i);
        });
    }
    
    Widget::~Widget() {
        delete ui;
    }
    
    void Widget::on_pushButton_clicked() {
        //如果定时器没有工作才启动
        if (_timer->isActive() == false) {
            _timer->start(100);
        }
    
        //非常复杂的数据处理,需要花费5s
        QThread::sleep(5);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    //widget.ui
    
    • 1

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ya1AvSrx-1662045421466)(C:\Users\84995\AppData\Roaming\Typora\typora-user-images\1662018717956.png)]

    运行结果发现,当数据处理结束5秒后,才开始计时。这样我们就不能计算数据处理所花费的时间了。这是为什么呢?

    因为这是个单线程的程序,定时器计时和数据处理不是并行处理,而是数据处理结束之后才开始定时器计时。而如果我们使用多线程,则这两个任务可以同时执行,我们就可以得到数据处理所耗费的时间了。

    使用多线程的好处

    • 使用多线程可以把占据长时间的任务放到后台去处理
    • 用户界面可以更加吸引人,例如用户点击了一个按键去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
    • 程序的运行可能加快。
    • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等了。

    什么时候该使用多线程

    大多情况下,要用到多线程的主要是需要处理大量的IO操作时或处理的情况需要花费大量的时间等等。例如:

    • 读写文件
    • 视屏图像的采集、处理、显示、保存等

    Qt4实现多线程的方法

    Qt4实现多线程的方法

    1.自定义一个线程类,继承于QThread

    class MyThread : public QThread {
    public:
        void run(); //线程处理函数(和主线程不在同一个线程,可以同时处理)
    
    signals:
        void isDone(); //线程处理数据结束发送这个信号
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.主线程中启动子线程,开始并行处理。注意,主线程不能直接调用子线程的run函数,而需要通过子线程类的对象调用start()函数间接低矮用run()函数。

    //主线程类中
    MyThread *_thread = new MyThread(this);
    _thread->start(); //通过子线程对象调用start间接调用子线程的run()函数
    
    • 1
    • 2
    • 3

    子线程的创建

    还是以刚才的背景为例,我们准备求得复杂信号处理的时长。我们县创建一个子线程类MyThread.

    注意以下几点:

    • void run()函数是protected继承的QThread的虚函数,它是线程处理函数实现的入口
    • run()函数不能直接调用,必须通过MyThread类实例化对象调用start()间接调用。
    //MyThread.h
    #ifndef MYTHREAD_H
    #define MYTHREAD_H
    
    #include 
    
    class MyThread : public QThread
    {
        Q_OBJECT
    public:
        explicit MyThread(QObject *parent = nullptr);
    
    signals:
        void isDone();
    
    protected:
        //QThread的虚函数
        //线程处理函数
        //不能直接调用,通过start()间接调用
        void run(); //线程处理实现入口
    
    public slots:
    };
    
    #endif // MYTHREAD_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    //MyThread.cpp
    #include "mythread.h"
    
    MyThread::MyThread(QObject *parent) : QThread(parent) {
    
    }
    
    void MyThread::run() {
        //复杂的数据处理,用sleep来模拟,大概耗费5s
        sleep(5);
    
        //处理结束,发送处理完成信号
        emit isDone();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    主线程中调用子线程

    注意:

    • 线程号是有限的,空闲的线程也会占用资源,我们使用完线程后也要将线程关闭。
    • 线程关闭可以用quit()函数来关闭,quit()会等待线程执行完当前工作再关闭线程
    • 关闭完线程之后,需要回收线程资源,**通过wait()来回收线程资源。**类似于Linux回收进程资源的wait和waitpid。
    //widget.h
    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include 
    #include 
    #include 
    #include 
    
    //需求,点击start按钮,显示定时器时间
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = 0);
        ~Widget();
    
    public slots:
        void DealIsDone();
    
    private slots:
        void on_pushButton_clicked();
    
    private:
        Ui::Widget *ui;
        QTimer *_timer = new QTimer(this);
        MyThread *_thread = new MyThread(this);
    };
    
    #endif // WIDGET_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    //widget.cpp
    #include "widget.h"
    #include "ui_widget.h"
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget) {
        ui->setupUi(this);
    
        //只要定时器启动,自动触发timeout信号,每次触发timeout信号更新LCD显示
        connect(_timer, &QTimer::timeout, [=]() {
           static int i = 0;
           i++;
           ui->lcdNumber->display(i);
        });
    
        //子线程数据处理结束会发送isDone信号,这时我们暂停定时器.
        connect(_thread, &MyThread::isDone, this, &Widget::DealIsDone);
    
    }
    
    Widget::~Widget() {
        delete ui;
    }
    
    //由于线程号是有限的,线程用完之后要将其关闭
    void Widget::DealIsDone() {
        _timer->stop();
        //停止线程
        _thread->quit();
        //回收子进程资源
        _thread->wait();
    }
    
    void Widget::on_pushButton_clicked() {
        //如果定时器没有工作才启动
        if (_timer->isActive() == false) {
            _timer->start(100);
        }
    
        //启动子线程同时进行数据处理
        _thread->start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    这样,我们就可以LCD显示和数据处理同时进行,这样就可以知道数据处理的时间了。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cD4UYfUA-1662045421467)(C:\Users\84995\AppData\Roaming\Typora\typora-user-images\1662024613522.png)]

    Qt5实现多线程的方法

    Qt5实现多线程的方法

    例:背景是,我们将定时器写成子线程,然后主线程就用来显示LCD的值。

    1.设定一个类,继承于QObject

    2.类中设置一个线程函数(只有一个线程函数)

    //MyThread
    class MyThread : public QObject {
    public:
        void myTimer() {
            while (1) {
                emit mySignal();
            }
        }
    signals:
        void mySignal(); //类似定时器的timeout信号
        void startThread(); //启动线程信号
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3.主线程中创建线程对象(不能指定父对象)

    4.主线程中创建一个QThread子线程对象

    5.把自定义线程类加入到子线程

    6.启动子线程,并发射启动子线程信号

    7.自定义线程函数通过与发射的子线程信号连接来实现子线程。(不能直接调用线程处理函数,直接调用线程处理函数,会导致线程处理函数和主线程在同一个线程,只能通过信号和槽的方式来调用)

    子线程的创建

    //mythread.h
    #define MYTHREAD_H
    
    #include 
    #include 
    #include 
    
    class MyThread : public QObject {
        Q_OBJECT
    public:
        explicit MyThread(QObject *parent = nullptr);
    
        //线程处理函数
        void dealTimeOut();
    
        void setFlag(bool flag);
    signals:
        void myTimeOut();
    public slots:
    
    private:
        bool _isStop = false;
    };
    
    #endif // MYTHREAD_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    //myThread.cpp
    #include "mythread.h"
    
    MyThread::MyThread(QObject *parent) : QObject(parent) {
    
    }
    
    void MyThread::dealTimeOut() {
        while (!_isStop) {
            //每隔1s发送一个信号
            QThread::sleep(1);
            emit myTimeOut();
            qDebug() << "子线程号:" << QThread::currentThread() << endl;
        }
    }
    
    void MyThread::setFlag(bool flag) {
        _isStop = flag;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    主线程调用子线程

    有以下注意事项:

    1. 分别创建子线程对象和自定义线程对象。注意自定义线程对象不能指定父类。
    2. 将自定义线程对象通过moveToThread函数移动到子线程对象中。
    3. 不能直接调用自定义线程运行函数,要调用自定义线程处理函数时,采用以下步骤:
      • 启动子线程
      • 主窗口发送启动线程的信号
      • 自定义线程要执行的处理函数作为槽函数被调用。
    4. 线程结束时,也要进行如下步骤:
      • 子线程调用quit()函数结束线程
      • 子线程调用wait回收子线程资源
      • 注意子线程未结束直接关闭主线程,子线程不会关闭,因此关闭主线程时也要把子线程关闭,并且由于自定义线程没有指定父对象,关闭时也要把自定义线程对象delete掉。
    //widget.h
    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include 
    #include "mythread.h"
    #include 
    #include 
    
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = 0);
        ~Widget();
    
         void dealSignal();
    
    private slots:
        void on_pushButtonStart_clicked();
    
        void on_pushButtonStop_clicked();
    
    signals:
        void startThread(); //启动子线程的信号
    
    private:
        Ui::Widget *ui;
        //动态分配空间不能指定父对象
        MyThread *_myThread = new MyThread;
    
        //创建子线程,指定父对象
        QThread *_thread = new QThread(this);
    };
    
    #endif // WIDGET_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    //widget.cpp
    #include "widget.h"
    #include "ui_widget.h"
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget) {
        ui->setupUi(this);
    
        //把自定义线程加入到子线程中
        _myThread->moveToThread(_thread);
    
        connect(this, &Widget::startThread, _myThread, &MyThread::dealTimeOut);
        connect(_myThread, &MyThread::myTimeOut, this, &Widget::dealSignal);
        connect(this, &Widget::destroyed, this, &Widget::on_pushButtonStop_clicked);
        qDebug() << "主线程号:" << QThread::currentThread() << endl;
    
    }
    
    Widget::~Widget() {
        delete ui;
    }
    
    void Widget::dealSignal() {
        static int i = 0;
        ui->lcdNumber->display(i++);
    }
    
    void Widget::on_pushButtonStart_clicked() {
        //启动线程,但没有启动线程处理函数
        _thread->start();
        _myThread->setFlag(false);
        //不能直接调用线程处理函数
        //直接调用,导致线程处理函数和主线程在同一个线程
    //    _myThread->dealTimeOut();
        //只能通过信号与槽的方式来调用
        emit startThread();
    }
    
    void Widget::on_pushButtonStop_clicked() {
        _myThread->setFlag(true);
        _thread->quit();
        _thread->wait();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    connect()的第五个参数的作用

    connect()第五个参数的作用,连接方式:默认,队列,直接,第五个参数只有在多线程时才有意义。

    • 如果是多线程,默认使用队列方式
    • 如果是单线程,默认使用直接方式
    • 队列:槽函数所在的线程和接受者一样。
    • 直接:槽函数所在线程和发送者一样。

    线程画图

    子线程不能操作图形界面,主要用于后台运行,进行数据处理使用。

    背景:子线程用于画图,主线程每次点击画图按钮,更新子线程画的图形

    子线程的创建

    子线程画完图之后,发射更新图片信号,将画好的图片通过信号传送出去。

    //mythread.h
    #ifndef MYTHREAD_H
    #define MYTHREAD_H
    
    #include 
    #include 
    #include 
    #include 
    
    class MyThread : public QObject {
        Q_OBJECT
    public:
        explicit MyThread(QObject *parent = nullptr);
    
        //线程处理函数
        void drawImage();
    
    signals:
        void updateImage(QImage img);
    
    public slots:
    
    };
    
    #endif // MYTHREAD_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    //mythread.cpp
    #include "mythread.h"
    
    MyThread::MyThread(QObject *parent) : QObject(parent) {
    
    }
    
    //线程处理函数
    void MyThread::drawImage() {
        //定义QImage绘图设备
        QImage image(500, 500, QImage::Format_ARGB32);
        //定义画家,指定绘图设备
        QPainter painter(&image);
    
        //定义画笔对象
        QPen pen;
        pen.setWidth(5);
        painter.setPen(pen);
    
        //定义画刷对象
        QBrush brush;
        brush.setColor(Qt::blue);
        brush.setStyle(Qt::Dense1Pattern);
        painter.setBrush(brush);
    
        //定义5个点
        QPoint a[5] {
            QPoint(qrand() % 500, qrand() % 500),
            QPoint(qrand() % 500, qrand() % 500),
            QPoint(qrand() % 500, qrand() % 500),
            QPoint(qrand() % 500, qrand() % 500),
            QPoint(qrand() % 500, qrand() % 500)
        };
    
    
        painter.drawPolygon(a, 5);
    
        //通过信号发送图片
        emit updateImage(image);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    主线程调用子线程

    主线程调用子线程步骤如下:

    1. 创建子线程对象和自定义函数处理对象。并将自定义函数处理对象moveToThread到创建出的子线程中。
    2. 开启子线程,_thread->start(),并通过connect连接线程处理函数。
    3. 子线程处理函数处理结束后,发送处理结束信号updateImage,主线程接受这个信号,并将接收到的图片画到设备上。
    4. 关闭子线程。
    //widget.h
    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include 
    #include 
    #include 
    #include 
    #include 
    
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = 0);
        ~Widget();
    
        //重写绘图事件
        void paintEvent(QPaintEvent *event);
    
    public slots:
        void slotGetImage(QImage img);
    
        void slotClose(); //窗口关闭槽函数
    
    private:
        Ui::Widget *ui;
        QImage _image;
        MyThread *_myThread = new MyThread;
        QThread *_thread = new QThread(this);
    };
    
    #endif // WIDGET_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    //widget.cpp
    #include "widget.h"
    #include "ui_widget.h"
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget) {
        ui->setupUi(this);
    
        //将自定义模块添加到子线程
        _myThread->moveToThread(_thread);
    
        //启动子线程,但是并没有启动线程处理函数
        _thread->start();
    
        //线程处理函数,必须通过signal-slot调用
        //drawImage槽函数和接受者_myThread在同一个子线程,而_myThread在子线程_thread中,因此就连接到了子线程。
        connect(ui->pushButton, &QPushButton::clicked, _myThread, &MyThread::drawImage);
        connect(_myThread, &MyThread::updateImage, this, &Widget::slotGetImage);
        connect(this, &Widget::destroyed, this, &Widget::slotClose);
    }
    
    Widget::~Widget() {
        delete ui;
    }
    
    void Widget::paintEvent(QPaintEvent *event) {
        QPainter painter(this); //指定绘图设备为窗口
        painter.drawImage(50, 50, _image);
    }
    
    void Widget::slotGetImage(QImage img) {
        _image = img;
        update(); //更新窗口,间接调用paintEvent
    }
    
    void Widget::slotClose() {
        //结束线程
        _thread->quit();
        //回收线程资源
        _thread->wait();
        //销毁自定义线程类对象
        delete _myThread;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
  • 相关阅读:
    Unity3D 打包发布时生成文件到打包目录
    C/C++字符函数和字符串函数详解————长度受限制的字符串函数
    基础 | JVM - [内存模型 & JMM]
    智慧职教解决方案-最新全套文件
    Virtualbox ArchLinux 安装
    Java高级:反射
    【算法练习Day31】单调递增的数字&&买卖股票的最佳时机含手续费&&监控二叉树
    Ubuntu 软件管理学习笔记
    家用小型洗衣机哪款性价比高?婴儿专用洗衣机推荐
    Mac M2/M3 芯片环境配置以及常用软件安装-前端
  • 原文地址:https://blog.csdn.net/weixin_44897707/article/details/126653255