QThread介绍

QThread介绍在程序设计中,为了不影响主程序的执行,常常把耗时操作放到一个单独的线程中执行。Qt对多线程操作有着完整的支持,Qt中通过继承QThread并重写run()方法的方式实现多线程代码的编写。针对线程之间的同步与互斥问题,Qt还提供了QMutex、QReadWriteLock、QwaitCondition、QSemaphore等多个类来实现。本篇博客将针对以下几个方面进行讲解[1]QThread的常用接口以及QThread的实现[2]QThread的信号事件[3]QThread执行完后自动释放内存

大家好,又见面了,我是你们的朋友全栈君。

在程序设计中,为了不影响主程序的执行,常常把耗时操作放到一个单独的线程中执行。Qt对多线程操作有着完整的支持,Qt中通过继承QThread并重写run()方法的方式实现多线程代码的编写。针对线程之间的同步与互斥问题,Qt还提供了QMutex、QReadWriteLock、QwaitCondition、QSemaphore等多个类来实现。

本篇博客将针对以下几个方面进行讲解

[1]QThread的常用接口以及QThread的实现

[2]QThread的信号事件

[3]QThread执行完后自动释放内存

[4]关闭窗口时自动停止线程的运行

[5]QThread的同步与互斥
[1]QThread的常用接口以及QThread的实现

定义Qthread需要执行的任务:

virtual void run()

编程者需要重写run()函数,在run函数里来实现线程需要完成的任务。

开始执行线程任务:

[slot] void QThread::start(QThread::Priority priority = InheritPriority)

线程休眠:

    //以下三个函数全部是静态成员函数
    void  msleep(unsigned long msecs)
    void  sleep(unsigned long secs)
    void  usleep(unsigned long usecs)

结束线程执行:

在run函数里主动结束:

    void  quit()
    void  exit(int returnCode = 0)

在任何位置强制线程结束:

[slot] void QThread::terminate()

不推荐此方法,除非万不得已。在调用此方法后还需调用wait()方法,来等待线程结束并回收资源。

线程优先级相关:

    //获取线程的优先级
    QThread::Priority  priority() const
     
    //设置线程的优先级
    void  setPriority(QThread::Priority priority)

判断是否运行:

    //判断是否运行结束
    bool  isFinished() const
     
    //判断是否正在运行
    bool  isRunning() const

QThread具体实现:

在这里通过模拟一个耗时的任务来进行说明,在QThread中模拟一个下载任务(每100ms计数+1,直到加到100为止),并在界面上通过QLabel显示出当前下载进度。实现一个自定义QThread的步骤如下:

①新创建类TestThread继承QThread

②重写run方法

③定义TestThread对象并调用该对象的start方法运行

TestThread.h代码如下:

    #ifndef TESTTHREAD_H
    #define TESTTHREAD_H
     
    #include <QObject>
    #include <QThread>
     
    class TestThread : public QThread
    {

        Q_OBJECT
    public:
        explicit TestThread(QObject *parent = nullptr);
     
    private:
        //重写run方法
        void run();
     
    signals:
        //定义信号
        void ShowDownloadProgress(int progress);
     
    public slots:
    };
     
    #endif // TESTTHREAD_H

TestThread.cpp代码如下:

    #include “testthread.h”
     
    TestThread::TestThread(QObject *parent) : QThread(parent)
    {

     
    }
     
    void TestThread::run()
    {

        for(int i = 0 ; i <= 100 ; i++)
        {

            QThread::msleep(100);
            ShowDownloadProgress(i);
        }
    }

其中,在run中进行线程任务的实现,当run函数执行完了,整个线程也就运行结束了。在run函数中用msleep来模拟耗时的过程,用i++来模拟下载进度的增加。每一次循环都会发出ShowDownloadProgress(i)信号,通过信号与槽的绑定,可以在Qt处理线程中完成QLabel数据的更新。

widget.cpp中线程对象的创建、信号与槽的绑定、线程启动代码如下:

    TestThread *thread = new TestThread(this);
    connect(thread,SIGNAL(ShowDownloadProgress(int)),this,SLOT(ProgressLabelShow(int)));
    thread->start();

ProgressLabelShow(int)槽函数的具体实现如下:

    void Widget::ProgressLabelShow(int prog)
    {

        ui->ProgressLabel->setText(QString::number(prog) + “%”);
    }

如上代码即实现了在界面上实时显示下载进度。之所以通过发出信号通知Qt处理线程,并在Qt处理线程中完成QLabel显示内容的更新是因为多线程同时操作Qt控件会有一定的危险,有可能导致程序的异常。而在TestThread线程中发出信号通知Qt处理线程,并在Qt处理线程中操作Qt控件的方法无论是在代码稳定性还是代码结构上都是最佳的。

运行效果:

[2]QThread的信号事件

QThread有两个信号事件,一个是线程开始时(run函数被调用之前发出此信号),发出来的,一个是线程结束时(在线程将要结束时发出此信号)。开始和结束信号如下:

    void finished()
    void started()

[3]QThread执行完后自动释放内存

QThread执行结束后自动释放内存,是利用finished信号实现的。官方提供的手册的finished信号的介绍中有这样一句话:

    When this signal is emitted, the event loop has already stopped running. No more events will be processed in the thread, except for deferred deletion events. This signal can be connected to QObject::deleteLater(),to free objects in that thread.

这句话的意思是将finished绑定到QObject::deleteLater()槽函数可以实现线程的自动销毁。

为了便于看到效果,我们给自定义的TestThread 类加上析构函数,并在里面打印提示信息:

    ~TestThread()
    {

        qDebug() << “~TestThread”;
    }

在widget.cpp中绑定finished信号与QObject::deleteLater():

    TestThread *thread = new TestThread(this);
    connect(thread,SIGNAL(ShowDownloadProgress(int)),this,SLOT(ProgressLabelShow(int)));
    connect(thread,SIGNAL(finished()),thread,SLOT(deleteLater()));
    thread->start();

其中,信号的发送者和接收者都是新创建的thread对象,槽函数为deleteLater(),该槽函数是继承自QObject的。

程序执行结果:

可以看到析构函数被自动执行,由此就完成了在线程结束后自动释放线程空间的功能。
[4]关闭窗口时自动停止线程的运行

前面有讲到在线程运行结束时自动释放线程控件,然而,在窗口关闭时。为了及时释放系统资源,也需要程序自动停止正在运行的线程,并释放掉空间。通过重写widget类的closeEvent方法可以实现这个目的:

改写TestThread类如下:

    #ifndef TESTTHREAD_H
    #define TESTTHREAD_H
     
    #include <QObject>
    #include <QThread>
    #include <QDebug>
     
    class TestThread : public QThread
    {

        Q_OBJECT
    public:
        explicit TestThread(QObject *parent = nullptr);
        ~TestThread()
        {

            qDebug() << “~TestThread”;
        }
     
        void StopThread();
     
    private:
        //重写run方法
        void run();
        bool stopFlag = false;
     
    signals:
        //定义信号
        void ShowDownloadProgress(int progress);
     
    public slots:
    };
     
    #endif // TESTTHREAD_H

 

    #include “testthread.h”
     
    TestThread::TestThread(QObject *parent) : QThread(parent)
    {

     
    }
     
    void TestThread::run()
    {

        for(int i = 0 ; i <= 100 && !stopFlag ; i++)
        {

            QThread::msleep(100);
            ShowDownloadProgress(i);
        }
    }
    void TestThread::StopThread()
    {

        stopFlag = true;
    }

其中,新加的stopFlag标志是为了控制线程是否结束,提供StopThread供外部调用。

在widget.cpp中重写closeEvent方法:

    void Widget::closeEvent(QCloseEvent *event)
    {

        qDebug() << “closeEvent”;
        TestThread *thread =  this->findChild<TestThread*>();
        if(thread == nullptr)
            return;
        if(thread->isRunning())
        {

            thread->StopThread();
            thread->wait();
        }
    }

在closeEvent中直接调用findChild方法得到先前创建的TestThread线程的指针,然后调用StopThread方法将线程的结束标志置为true,最后调用wait方法阻塞等待线程结束。

运行结果如下:

 
[5]QThread的同步与互斥

在多线程编程中,常常会有某些资源被多个线程共用的情况。例如多个线程需要读/写同一个变量,或者一个线程需要等待另一个线程先运行后才可以运行。进程的同步与互斥,在多线程编程中尤为重要。用的好了,既能让程序稳定运行,又能不影响程序运行效率。用的不好就可能导致程序虽然在稳定运行,但效率大大下降。究其原因,编程者在编程时要明确知道应该用什么同步互斥机制,如何去用这些同步互斥机制。对于线程的同步与互斥Qt提供了QMutex、QReadWriteLock、QwaitCondition、QSemaphore等多个类来实现。

互斥锁:

QMutex是基于互斥量的线程同步类,QMutex类主要提供了以下几个方法,用于实现互斥操作:

    lock():上锁,如果之前有另一个进程也针对当前互斥量进行了上锁操作,则此函数将一直阻塞等待,直到解锁这个互斥量。

    unlock():解锁,与lock()成对出现。

    tryLock():尝试解锁一个互斥量,该函数不会阻塞等待,成功返回true,失败返回false(其他线程已经锁定了这个互斥量);

下面是一个利用互斥量来实现的例子:

    int flag;
    QMutex mutex;
     
    void threadA::run()
    {

        ….
        mutex.lock();
        flag = 1;
        mutex.unlock();
        ….
    }
     
    void threadB::run()
    {

        ….
        mutex.lock();
        flag = 2;
        mutex.unlock();
        ….
    }
     
    void threadC::run()
    {

        ….
        mutex.lock();
        flag = 3;
        mutex.unlock();
        ….
    }

利用互斥锁保护的资源,不允许多个线程同时操作。

读写锁:

互斥锁会在某些应用中出现问题,例如多个线程需要去读某一个变量。此时是不需要排队的,可以同时进行读操作。如果用互斥锁来做保护,这会导致不必要的排队现象发生,影响到程序的运行效率。这时,就需要引入读写锁QReadWriteLock。

QReadWriteLock提供了以下几个方法:

lockForRead():以只读方式锁定资源,其他线程可读(可以调用lockForRead),不可写(调用lockForWrite将阻塞等待)。如果先前有其他线程以写锁方式进行了锁定,则调用这个函数会阻塞等待

lockForWrite():以写入方式锁定资源,其他线程不可读,不可写。如果先前有其他线程以读锁或写锁的方式进行了锁定,调用这个函数会阻塞等待。

unlock()解锁,与锁定资源函数成对出现。

tryLockForRead():lockForRead的非阻塞版本。

tryLockForWrite():lockForWrite的非阻塞版本。

下面是一个用读写锁的例子:

    int flag;
    QReadWriteLock rwLock;
     
    void threadA::run()
    {

        ….
        rwLock.lockForWrite();
        flag = 1;
        rwLock.unlock();
        ….
    }
     
    void threadB::run()
    {

        ….
        rwLock.lockForWrite();
        flag = 2;
        rwLock.unlock();
        ….
    }
     
    void threadC::run()
    {

        ….
        rwLock.lockForRead();
        switch(flag)
        {

            ……
        }
        rwLock.unlock();
        ….
    }
    void threadD::run()
    {

        ….
        rwLock.lockForRead();
        qDebug() << flag;
        ……
        rwLock.unlock();
        ….
    }

利用读写锁保护的资源,允许多个线程同时读,不允许多个线程在读的同时写,不允许在写的同时读或写。

基于QWaitCondition的线程同步:

 前面所提到的互斥锁、读写锁,都是通过加锁的方式实现的资源的保护。在资源解锁时,其他线程并不会立刻得到通知。针对这个问题,Qt引入了QWaitCondition类。将QWaitCondition与QMutex或QReadWriteLock相结合可以实现在资源解锁后及时通知并唤醒其他等待进程。

QWaitCondition提供的方法如下:

    wait(QMutex *lockedMutex, unsigned long time = ULONG_MAX)
    wait(QReadWriteLock *lockedReadWriteLock, unsigned long time = ULONG_MAX):解锁互斥锁或读写锁,并阻塞等待被唤醒。当被唤醒后,重新锁定QMutex或QReadWriteLock

    wakeAll():唤醒所有等待的进程,顺序不确定,由操作系统调度
    wakeOne():唤醒一个等待的进程,唤醒哪一个不确定,由操作系统调度

QWaitCondition常用于生产/消费者中,一个产生数据的,几个消费数据的。比如键盘的输入,当键盘输入数据后,有多个线程同时对键盘输入的数据做不同的处理,此时就需要用到QWaitCondition来实现。

全局可用变量的定义

    QWaitCondition keyPressed;
    char c;
    int count;

线程1:获取键盘的输入

    for(;;){

          c = getchar();
     
          mutex.lock();
          // Sleep until there are no busy worker threads
          while (count > 0) {

              mutex.unlock();
              sleep(1);
              mutex.lock();
          }
          keyPressed.wakeAll();
          mutex.unlock();
      }

线程2:处理输入数据

     for(;;){

          mutex.lock();
          keyPressed.wait(&mutex);
          ++count;
          mutex.unlock();
     
          do_something_xxxx(c);
     
          mutex.lock();
          –count;
          mutex.unlock();
      }

线程3:处理输入数据

     for(;;){

          mutex.lock();
          keyPressed.wait(&mutex);
          ++count;
          mutex.unlock();
     
          do_something_xxxxxxxxxxxxx(c);
     
          mutex.lock();
          –count;
          mutex.unlock();
      }

在本例的线程1中引入了count 是否大于 0的判断,是为了保证每个线程都能够执行完后,再进行键盘输入获取以及唤醒操作。

利用信号量(QSemaphore)实现的线程同步:

互斥锁、共享锁都只能针对一个资源进行保护,而不能针对多个类似的资源进行保护。而利用QSemaphore可以做到对多个类似的资源进行保护。

QSemaphore主要提供了以下几个方法:

    acquire(int n = 1):获取n个资源,如果没有,则阻塞等待,直到有n个资源可用为止。

    release(int n = 1):释放更多资源,如果信号量的资源已全部可用后,调用此函数将增加更多的资源

    bool tryAcquire(int n = 1):尝试获取n个资源,不会阻塞等待,有返回true,无返回false

简单示例:

    QSemaphore sem(5);      // sem.available() == 5
     
    sem.acquire(3);         // sem.available() == 2
    sem.acquire(2);         // sem.available() == 0
    sem.release(5);         // sem.available() == 5
    sem.release(5);         // sem.available() == 10
     
    sem.tryAcquire(1);      // sem.available() == 9, returns true
    sem.tryAcquire(250);    // sem.available() == 9, returns false

示例:

定义的全局变量

    const int DataSize = 100000;
     
    const int BufferSize = 8192;
    char buffer[BufferSize];
     
    QSemaphore freeBytes(BufferSize);
    QSemaphore usedBytes;

生产者线程:

    class Producer : public QThread
      {

      public:
          void run() override
          {

              for (int i = 0; i < DataSize; ++i) {

                  freeBytes.acquire();
                  buffer[i % BufferSize] = “ACGT”[QRandomGenerator::global()->bounded(4)];
                  usedBytes.release();
              }
          }
      };

消费者线程:

    class Consumer : public QThread
      {

          Q_OBJECT
      public:
          void run() override
          {

              for (int i = 0; i < DataSize; ++i) {

                  usedBytes.acquire();
                  fprintf(stderr, “%c”, buffer[i % BufferSize]);
                  freeBytes.release();
              }
              fprintf(stderr, “\n”);
          }
      };

这个示例展示了生产者要产生10万个数据,并循环放进8192大小的缓存区中,消费者同时去取缓存区数据。在生产者放的过程中,只能放置到未使用的空间或经过消费者处理过的空间中。

信号量的引入保证了数据的读写的效率,也保证了消费者能够完整的拿到所有数据。而此例如果用互斥锁或读写锁实现的话效率将大打折扣(生产者:上锁(等待)—-写满缓冲区—–解锁   消费者:上锁(等待)—–读缓冲区—–解锁),针对一个有多个字节的数据缓冲区读写不能同时进行。而使用信号量一边写未被写过的或已经被处理过的空间,一边将已写过的空间交给读进程操作将使程序效率大大提高。
 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/140368.html原文链接:https://javaforall.net

(0)
上一篇 2022年5月28日 上午9:40
下一篇 2022年5月28日 上午9:40


相关推荐

  • Java面向对象三大特性详解「建议收藏」

    Java面向对象三大特性详解「建议收藏」一、封装1、概念:将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。2、好处:只能通过规定的方法访问数据。 隐藏类的实例细节,方便修改和实现。3、封装的实现步骤     需要注意:对封装的属性不一定要通过get/set方法,其他方法也可以对封装的属性进行操作。当然最好使用get/set方法,比较标准。A、访问修饰…

    2022年7月25日
    10
  • 基础知识篇——堆内存和栈内存

    基础知识篇——堆内存和栈内存数据结构中的堆和栈栈是一种连续储存的数据结构 具有先进后出的性质 通常的操作有入栈 压栈 出栈和栈顶元素 想要读取栈中的某个元素 就是将其之间的所有元素出栈才能完成 堆是一种非连续的树形储存数据结构 每个节点有一个值 整棵树是经过排序的 特点是根结点的值最小 或最大 且根结点的两个子树也是一个堆 常用来实现优先队列 存取随意 内存中的栈区与堆区 Stackmemory 内存空间由操

    2026年3月17日
    2
  • PHP调用大模型API实战指南:从基础封装到生产部署

    PHP调用大模型API实战指南:从基础封装到生产部署

    2026年3月12日
    3
  • jtl转换成html,Jmeter的jtl文件转换HTML报告[通俗易懂]

    jtl转换成html,Jmeter的jtl文件转换HTML报告[通俗易懂]Jmeter我们用做接口测试的时候使用可视化界面,可以通过查看结果树或者聚合报告来观看结果。但是,在工作中,这样的结果,往往也要成为你测试报告的一部分,傻的办法当然是截图,然后展示,这样的傻办法,我用了几次就不想再用,一次截图次数太多,让人看的眼花缭乱,不能很好的体现我们测试报告的准确性和说服力。做为一个,测试开发,就要来解决这样的问题,尽量把重复工作降低,提升工作效率。通过参考,我找到了Jmet…

    2025年7月2日
    4
  • MySQL与SqlServer的区别「建议收藏」

    MySQL与SqlServer的区别「建议收藏」一、MySQL与SqlServer的区别目前最流行的两种后台数据库即为Mysql和SQLServer。这两者最基本的相似之处在于数据存储和属于查询系统,你可以使用SQL来访问这两种数据库的数据,因为它们都支持ANSI-SQL(数据库管理标准)。还有,这两种数据库系统都支持二进制关键字和关键索引,这就大大地加快了查询速度。同时,二者也都提供支持XML的各种格式。根本的区别:SQL服务器的狭隘的、保守的存储引擎而MySQL服务器的可扩展、开放的存储引擎;SQL服务器的引擎是Sybase,而MyS

    2022年10月2日
    3
  • 我的 Anthropic Claude Code 學習筆記

    我的 Anthropic Claude Code 學習筆記

    2026年3月15日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号