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大小的缓存区中,消费者同时去取缓存区数据。在生产者放的过程中,只能放置到未使用的空间或经过消费者处理过的空间中。

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

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

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • python文件操作的相关函数_python 高级

    python文件操作的相关函数_python 高级前言:本博文主要讲解Python文件操作的写法,属于Python高级语法。基础语法见:深入浅出Python——Python基础语法全解,更多内容请访问博主的主页,谢谢!文章目录一、文件操作的作用二、文件的基本操作1.文件操作步骤1.1打开1.1.1打开文件模式1.1.2快速体验2.1文件对象方法1.2.1写1.2.2读1.2.3seek()3.1关闭三、文件备份1.步骤2.代码实现3.思考四、文件和文件夹的操作1.文件重命名2.删除文件3.创建文件夹4.删除文件夹5.获

    2022年10月17日
  • angular 路由懒加载_angular路由

    angular 路由懒加载_angular路由angular8路由懒加载在angular中路由即能加载组件又能加载模块,而我们说的懒加载实际上就是加载模块,目前还没有看到懒加载租价的例子。加载组件使用的是component关键字加载模块则是使用loadChildren关键字例子代码父模块路由文件import{NgModule}from’@angular/core’;import{Routes,RouterMo…

  • pycharm 多行注释_注释代码快捷键是什么

    pycharm 多行注释_注释代码快捷键是什么多行注释选中代码后快捷键Ctrl+/单行注释选中代码或者光标停留在该行,然后使用快捷键Ctrl+/多行代码缩进选中代码后,快捷键Tab多行代码取消缩进选中代码后,快捷键shift+Tab…

  • Linux中vi命令用法「建议收藏」

    Linux中vi命令用法「建议收藏」Linux中vi命令的用法

  • xamarin android listview的用法

    xamarin android listview的用法listview也许是用的非常频繁的一个控件之一,下面我写一个xamarin的listview栗子,大家尝一尝xamarinandroid开发的乐趣。原谅我的大小写吧.listview绑定自定义的BaseAdapter先来看一下最终实现的效果图:News.cs和NewAdapter.csnamespaceDrawerLayout.Adapter{public…

  • 【手把手】制作一个简单的HTML网页

    【手把手】制作一个简单的HTML网页新建一个html文件:我要给body添加一些样式,就在head元素上挂载一个style元素。然后,写样式表:效果:这样的话,我们就可以看到body部分了。现在,我给body添加一点padding。页面上看不出区别,让我们打开F12,看一下现在的body变成了什么样子。示意图:从图中可以看出,body元素分为三层,最里面的一层,就是520×500的那部分,这个叫做内部真实空间,也就是说,你可以在里面添加其他元素,比如div元素,p元素,a元素等等。然后中间的一层就是paddin

发表回复

您的电子邮箱地址不会被公开。

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