在程序设计中,为了不影响主程序的执行,常常把耗时操作放到一个单独的线程中执行。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类。将QWaitConditionQMutexQReadWriteLock相结合可以实现在资源解锁后及时通知并唤醒其他等待进程。

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 threadswhile (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() == 5sem.acquire(3);         // sem.available() == 2
sem.acquire(2);         // sem.available() == 0
sem.release(5);         // sem.available() == 5
sem.release(5);         // sem.available() == 10sem.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_OBJECTpublic: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大小的缓存区中,消费者同时去取缓存区数据。在生产者放的过程中,只能放置到未使用的空间或经过消费者处理过的空间中。

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

Qt之QThread介绍(常用接口及实现、自动释放内存、关闭窗口时停止线程运行、同步互斥)相关推荐

  1. qt 对话框关闭以及自动释放内存

    关于窗口关闭的操作,在这里指出常用的三个槽,即quit(),exit()以及close(). 首先说明窗口退出时,系统提示对话框的代码编辑.对主程序的退出,可以调用成员函数exit(),同时也可以调用 ...

  2. 最新QT从入门到实战完整版(08.qt中的坐标系-09 信号和槽-点击按钮关闭窗口_)

    最新QT从入门到实战完整版(08.qt中的坐标系-09 信号和槽-点击按钮关闭窗口_) 一.08.qt中的坐标系 二,09 信号和槽-点击按钮关闭窗口_ 来自 一.08.qt中的坐标系 二,09 信号 ...

  3. qt 如何 指针 自动 释放内存_要是面试官再问你智能指针的问题,就拿这篇文章“盘他”!!!...

    前一段时间,有不少朋友问我关于智能指针的问题,并且反映经常会在面试中被面试官问到,所以今天小豆君就来讲讲我对智能指针的理解,希望能对大家有所帮助 既然讲智能指针,我们就先来看看它为什么会出现. 1 传 ...

  4. 使用Qt编辑关闭窗口程序的一些见解

    http://sunshine1106.blog.51cto.com/1371108/305106/  近日在测试软件功能时,发现一些不是太显眼问题,如希望在点击窗口右上角的X按钮关闭窗口时,能够提示 ...

  5. 拦截QT关闭窗口的CloseEvent

    写程序遇到了QTimer启动以后可以在普通函数(例如槽函数)里面停止,但是无法在析构函数里停止的问题.想方设法皆无果,最后之得从关闭窗口上下功夫,关闭窗口时判断如果timer还是启动状态(即后台进程还 ...

  6. Qt5.9关闭子窗口时执行特定代码(析构函数、关闭窗口)(setAttribute(Qt::WA_DeleteOnClose)、closeEvent(QCloseEvent* event))

    本文主要总结在关闭qt的QWidget子窗口瞬间,执行特定代码.由于主窗口关闭时,会自动执行析构函数,而子窗口关闭时,却不会调用析构函数,进过博主查阅资料,发现有两种方法可以在子窗口关闭时,调用析构函 ...

  7. JAVA中常用接口的介绍及使用示例 java lang Comparable

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! JAVA ...

  8. 嵌入式--LCD常用接口介绍

    目录 LCD 分类 LCD常用接口模式介绍 RGB模式 SPI模式 MDDI模式 MIPI-DSI模式 MCU模式 8080 6800 VSYNC模式  LCD(Liquid Crystal Disp ...

  9. 液晶常用接口“LVDS、TTL、RSDS、TMDS”技术原理介绍

    液晶常用接口"LVDS.TTL.RSDS.TMDS"技术原理介绍 1:Lvds Low-Voltage Differential Signaling 低压差分信号 1994年由美国 ...

  10. Qt之QThread(深入理解)

    简述 为了让程序尽快响应用户操作,在开发应用程序时经常会使用到线程.对于耗时操作如果不使用线程,UI界面将会长时间处于停滞状态,这种情况是用户非常不愿意看到的,我们可以用线程来解决这个问题. 前面,已 ...

最新文章

  1. 2020 年技术趋势一览:AutoML、联邦学习、云寡头时代的终结
  2. python复制文件夹不阻塞_Python学习第54天(阻塞(blocking) IO和非阻塞(non-blocking)IO)...
  3. html应用中心模板,HTML5--应用网页模板
  4. boost::fusion::as_map用法的测试程序
  5. android项目方法数超过65536的解决办法
  6. EMR StarRocks 极速数据湖分析原理解析
  7. 3004基于二叉链表的二叉树的双序遍历(附题意解释)
  8. Spring-tx-TransactionAttributeSource接口
  9. 【安全风险通告】Apache ShardingSphere远程代码执行漏洞安全风险通告
  10. iOS与JS交互之UIWebView协议拦截
  11. Ex2010学习(十),恢复用户已删除邮件
  12. 网管员的任务与职责漫谈
  13. Slurm的前处理prolog和后处理epilog
  14. 小白如何快速绘制原型图
  15. java模拟选课_模拟学生选课系统的练习
  16. 黎曼Zeta函数,人类文明黎曼Zeta函数,人类文明永恒的的纪念
  17. 前端的工程化、模块化和组件化
  18. 【建议收藏】数据结构和算法面试题
  19. 利用SPSS可视化分箱轻松给数据进行等分分组
  20. 为什么oracle依旧是很多大公司数据库首选?

热门文章

  1. 计算机网络应用基础试卷B,计算机网络应用基础试卷
  2. 计算机取证程序论文,计算机取证论文参考文献推荐 计算机取证论文参考文献哪里找...
  3. 《OceanBase 数据库系统概念》首次发布,系统精准定义 OceanBase
  4. 用 Dev-C++ 编写简单的平均数/中位数/众数/方差/一元线性回归方程计算器(附带控制台颜色设置,选择界面)
  5. 你在公司项目里面看过哪些操蛋的代码?
  6. web服务器ngix基础
  7. Axure8.0的注册码
  8. 金融行业的JAVA软件开发
  9. Redis安装(Windows环境下Redis安装)
  10. 定投计算器和年化利率计算器