大家好,又见面了,我是你们的朋友全栈君。
在程序设计中,为了不影响主程序的执行,常常把耗时操作放到一个单独的线程中执行。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.cn/140368.html原文链接:https://javaforall.cn