qt线程同步
Qt提供了丰富的线程同步机制来帮助开发者更高效和安全地进行多线程编程。其主要包括:
-
QMutex:为共享数据提供互斥访问能力,避免同时写入导致的数据冲突。利用lock()/unlock()方法实现锁定和解锁。
-
QReadWriteLock:读写锁,允许多个读线程同时访问,但写操作需要独占锁。适用于读操作频繁,写操作较少的场景。
-
QSemaphore:信号量,用来限制特定资源的访问量。例如一次最多N个线程可以访问缓冲区。通过acquire()/release()实现获取和释放许可。
-
QWaitCondition:条件变量,可以让线程等待一个条件满足后被唤醒。配合QMutex完成更为复杂的同步需求。
-
Qfutures/QtConcurrent:提供了基于线程池的并行计算能力。将耗时任务放入线程池,自动进行并行执行与结果汇总。
-
QEventLoop/Signals&Slots:事件循环与信号槽机制可以实现复杂的线程间通信,例如定时器、Progress Reporting等。
-
QThread:Qt原生的线程类,通过moveToThread()实现在线程间安全地传递QObject。
互斥锁
QMutex / QMutexLocker
QMutex 是 Qt 中用于实现互斥锁的类,用于保证在多线程程序中访问共享资源的互斥性。它提供了两个基本操作:lock() 和 unlock(),分别用于加锁和解锁。
QMutexLocker 是 QMutex 的 RAII 风格封装,可以自动释放锁资源,避免忘记解锁而导致的死锁情况。QMutexLocker 在创建时会自动调用 QMutex 的 lock() 方法,析构时会自动调用 QMutex 的 unlock() 方法。因此使用 QMutexLocker 可以大大减少忘记解锁的情况。
QMutex mutex;
void func() {
{
QMutexLocker locker(&mutex);
//临界区域
}
mutex.lock();
//临界区域
mutex.unlock();
}
互斥锁实现买票同步程序
此处实现多线程重写版完美退出机制
#ifndef SELLER_H
#define SELLER_H
#include<QThread>
#include<QMutex>
#include<QDebug>
class seller :public QThread
{
Q_OBJECT
public:
seller()=default;
seller(int* data,QMutex *m){
tickets=data;
mtx=m;
}
signals:
void ticketsFinsh();
public:
void run() override{
while ((*tickets) > 0) { //多线程经典问题 双锁机制
mtx->lock();
if((*tickets) > 0){
mtx->unlock();
mtx->lock();
qDebug()<<this->objectName()<<" : " <<--(*tickets);
mtx->unlock();
QThread::usleep(100);
}
}
ticketsFinsh();
}
private:
int* tickets; // 票
QMutex *mtx;
};
#endif // SELLER_H
#include <QCoreApplication>
#include"seller.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
int tickets =100;
QMutex mtx;
seller* t1 =new seller(&tickets,&mtx);
seller* t2 =new seller(&tickets,&mtx);
seller* t3 =new seller(&tickets,&mtx);
t1->setObjectName("Thread 1");
t2->setObjectName("Thread 2");
t3->setObjectName("Thread 3");
//设置线程完成信号与主线程进行连接,主线程知道子线程任务结束。
QObject::connect(t1, &seller::finished, t1, &seller::quit);
QObject::connect(t2, &seller::finished, t2, &seller::quit);
QObject::connect(t3, &seller::finished, t3, &seller::quit);
//线程对象在离开线程后删除,但不代表线程任务真正完成。
QObject::connect(t1, &seller::finished, t1, &seller::deleteLater);
QObject::connect(t2, &seller::finished, t2, &seller::deleteLater);
QObject::connect(t3, &seller::finished, t3, &seller::deleteLater);
//线程对象在离开线程 结束应用的通知
QObject::connect(t1, &seller::finished, &a, &QCoreApplication::quit);
QObject::connect(t2, &seller::finished, &a, &QCoreApplication::quit);
QObject::connect(t3, &seller::finished, &a, &QCoreApplication::quit);
//start与run的区别:调用run()直接在当前线程执行任务代码。 start()用于启动一个线程,将任务移动到新线程运行。它会调用run()函数,但run()此时会在新线程的上下文中执行。
t1->start();
t2->start();
t3->start();
t1->wait();
t2->wait();
t3->wait();
return a.exec();
}
QSemaphore 信号量
成员函数
acquire(int n = 1):从信号量中获取n个信号量(如果当前信号量可用)。如果数量不足,则阻塞等待其可用。成功返回后信号量减少n个。
release(int n = 1):向信号量中增加n个信号量。其他正在等待的线程可能因此被唤醒。
tryAcquire(int n = 1):尝试从信号量中获取n个信号量,,但不阻塞等待。如果成功返回true并减去n,否则返回false。
available():返回当前信号量的数量。
cancelAcquire(n):取消acquire操作的等待,释放阻塞的线程。
tryAcquire(int n,int timeout):尝试获取n个信号量,等待最长timeout毫秒,成功或超时均返回。
tryAcquire(QSemaphore::Time timeout):tryAcquire的重载版本,等待时间采用Time类型。
这些方法提供了完整的获取/释放机制,开发者可以根据不同的同步需求灵活选择:
acquire()用于需要阻塞等待的情况;
tryAcquire()用于非阻塞获取,如有限资源池;
设置超时tryAcquire()可以防止死锁。
基于信号量实现一个家庭买票
#ifndef SELLER_H
#define SELLER_H
#include<QThread>
#include<QSemaphore>
#include<QtDebug>
#include<QMutex>
class seller : public QObject
{
Q_OBJECT
public:
seller()=default;
seller(QSemaphore* sem) : sem(sem) {i=0;}
signals:
void emptyTicket();
public slots:
void display(){
while(sem->tryAcquire(3)) {
qDebug() << this->objectName() << "售出:"<<++i<<"个家庭"<<" 剩余票数:"<<sem->available();
QThread::yieldCurrentThread(); //让出当前时间片
}
emit emptyTicket();
}
private:
int i;
QSemaphore *sem;
};
#endif // SELLER_H
#include <QCoreApplication>
#include"seller.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QThread t1,t2,t3; //3个工作人员售票
/*
* 空串原因
t1.setObjectName("Thread1");
t2.setObjectName("Thread 2");
t3.setObjectName("Thread 3");
*/
QSemaphore sem(99); // 33个家庭,每个家庭3个人 99张票 以家庭为单位售卖
seller* s1 =new seller(&sem);
s1->moveToThread(&t1); //开辟新的线程去执行 槽函数也是在新的线程执行
seller* s2 =new seller(&sem);
s2->moveToThread(&t2);
seller* s3 =new seller(&sem);
s3->moveToThread(&t3);
//因为class类this 是s对象
s1->setObjectName("Seller1");
s2->setObjectName("Seller2");
s3->setObjectName("Seller3");
QObject::connect(&t1,&QThread::started,s1,&seller::display);
QObject::connect(&t2,&QThread::started,s2,&seller::display);
QObject::connect(&t3,&QThread::started,s3,&seller::display);
QObject::connect(s1,&seller::emptyTicket,&t1,&QThread::quit);
QObject::connect(s2,&seller::emptyTicket,&t2,&QThread::quit);
QObject::connect(s3,&seller::emptyTicket,&t3,&QThread::quit);
QObject::connect(s1,&seller::emptyTicket,s1,&seller::deleteLater);
QObject::connect(s2,&seller::emptyTicket,s2,&seller::deleteLater);
QObject::connect(s3,&seller::emptyTicket,s3,&seller::deleteLater);
QObject::connect(&t1,&QThread::finished,&t1,&QThread::deleteLater);
QObject::connect(&t2,&QThread::finished,&t2,&QThread::deleteLater);
QObject::connect(&t3,&QThread::finished,&t3,&QThread::deleteLater);
t1.start();
t2.start();
t3.start();
// 等待所有线程完成
t1.wait();
t2.wait();
t3.wait();
return a.exec();
}
QWaitCondition 条件变量
QWaitCondition 是 Qt 框架中用于线程间同步的类之一。它允许一个线程等待另一个线程发出信号,从而实现线程间的协调和同步。 (相当于c11的条件变量)
在使用 QWaitCondition 时,通常会配合使用 QMutex。QMutex 用于保护共享资源,而 QWaitCondition 则用于在等待某个条件为真时挂起线程,并在条件满足时唤醒线程。
成员函数
void wait(QMutex *mutex):
将当前线程挂起,并释放指定的 QMutex。当某个其他线程调用 wakeOne() 或 wakeAll() 时,该线程将被唤醒。
当线程被唤醒时,它会重新获取 QMutex。
bool wait(QMutex *mutex, unsigned long time):
与上一个函数类似,但是带有超时时间参数。如果在指定的时间内没有被唤醒,该函数将返回 false。
返回 true 表示正常被唤醒,返回 false 表示超时。
void wakeOne():
唤醒一个正在等待的线程。如果没有线程正在等待,该函数什么也不做。
当有多个线程在等待时,哪个线程被唤醒是不确定的。
void wakeAll():
唤醒所有正在等待的线程。
所有等待的线程都将被唤醒,并重新获取相关的 QMutex。
基于条件遍历实现一个售票店与制票商供应程序
#ifndef PRODUCWORKER_H
#define PRODUCWORKER_H
#include"SellerWorker.h"
//模拟生产
class ProducWorker : public QObject {
Q_OBJECT
private:
QQueue<int> *curTicket;
QMutex* mtx;
QWaitCondition* cond;
int *curTicketid ;
public:
ProducWorker()=default;
ProducWorker(QQueue<int> *curTicket,
QMutex* mtx,
QWaitCondition* cond,
int *curTicketid){
this->curTicket=curTicket;
this->mtx=mtx;
this->cond=cond;
this->curTicketid=curTicketid;
}
~ProducWorker()=default;
public slots:
void work() {
while (1) {
{
QMutexLocker lock(mtx);
if (curTicket->length() < 20) { //假设最大生产20张票
int data=++(*curTicketid);
qDebug() << "生产票:" <<data ;
curTicket->push_back(data);
cond->wakeAll(); //通知商店售票一张
} else {
cond->wait(mtx); //超过则等待销售
}
QThread::usleep(1000);
}
}
}
};
#endif // PRODUCWORKER_H
#ifndef SELLERWORKER_H
#define SELLERWORKER_H
#include <QWaitCondition>
#include <QThread>
#include <QMutex>
#include <QQueue>
#include <QDebug>
//模拟售票
class SellerWorker : public QObject {
Q_OBJECT
private:
QQueue<int> *curTicket;
QMutex* mtx;
QWaitCondition* cond;
int *curTicketid ;
public:
SellerWorker()=default;
SellerWorker(QQueue<int> *curTicket,
QMutex* mtx,
QWaitCondition* cond,
int *curTicketid){
this->curTicket=curTicket;
this->mtx=mtx;
this->cond=cond;
this->curTicketid=curTicketid;
}
~SellerWorker()=default;
public slots:
void work() {
while (1) {
{
QMutexLocker lock(mtx);
if (!curTicket->isEmpty()) {
qDebug() << "售票:" << curTicket->front();
curTicket->pop_front();
cond->wakeOne(); //通知生产者 生产一张
} else {
cond->wait(mtx);
}
QThread::usleep(1000);
}
}
}
};
#endif // SELLERWORKER_H
#include <QCoreApplication>
#include"ProducWorker.h"
#include"ProducWorker.h"
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
QQueue<int> curTicket;
QMutex mtx;
QWaitCondition cond;
int curTicketid =0;
QThread t1, t2;
SellerWorker *sellerWorker =new SellerWorker(&curTicket,&mtx,&cond,&curTicketid);
ProducWorker *producWorker =new ProducWorker(&curTicket,&mtx,&cond,&curTicketid);
sellerWorker->moveToThread(&t1);
producWorker->moveToThread(&t2);
QObject::connect(&t1, &QThread::started, sellerWorker, &SellerWorker::work);
QObject::connect(&t2, &QThread::started, producWorker, &ProducWorker::work);
t1.start();
t2.start();
return a.exec();
}
QReadWriteLock 读写锁
QReadWriteLock 是 Qt 提供的用于读写操作的锁类,允许多个线程同时读取共享数据,提高了并发性能,但在写操作时会阻止其他的读取和写入,以确保数据的一致性。
void lockForRead():
获取读锁。如果已有写锁被持有,当前线程将被阻塞,直到获得读锁。
可以被多个线程同时调用,只要没有写锁被持有。
void lockForWrite():
获取写锁。如果已有读锁或写锁被持有,当前线程将被阻塞,直到获得写锁。
一次只允许一个线程持有写锁。
void unlock():
释放当前持有的读锁或写锁。
如果有其他线程正在等待锁,它们将被唤醒。
bool tryLockForRead(int timeout = 0):
尝试获取读锁。如果成功获取,返回 true。
如果在指定的超时时间内无法获取读锁,返回 false。
bool tryLockForWrite(int timeout = 0):
尝试获取写锁。如果成功获取,返回 true。
如果在指定的超时时间内无法获取写锁,返回 false。
基于读写锁实现3个写线程和5个读线程的同步
#ifndef CACHEMANAGER_H
#define CACHEMANAGER_H
#include <QDebug>
#include <QReadWriteLock>
#include <QThread>
#include <QVector>
class CacheManger : public QObject
{
Q_OBJECT
public:
CacheManger() {}
virtual ~CacheManger() {}
CacheManger( QVector<int> * data,QReadWriteLock* rwLock) :m_data(data),m_rwLock(rwLock){}
void readData(int index) {
while(1){
m_rwLock->lockForRead();
qDebug() << "Reading data at index:" << index << "- Data:" << m_data->at(index);
m_rwLock->unlock();
}
}
void writeData(int &sum) { //sum
int &value = sum;
while (1) {
m_rwLock->lockForWrite();
value+=1;
int index =value % 5;
(*m_data)[index] = value;
qDebug() << "Writing data at index:" << index << "- Data:" << value;
m_rwLock->unlock();
QThread::usleep(1000);
}
}
private:
QVector<int> * m_data ;
QReadWriteLock* m_rwLock;
};
#endif // CACHEMANAGER_H
#include <QCoreApplication>
#include"CacheManager.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QVector<int> m_data = {1, 2, 3, 4, 5};
QReadWriteLock m_rwLock;
for(int i=0;i<5;i++){
QThread* t =new QThread;
CacheManger* read = new CacheManger(&m_data,&m_rwLock);
read->moveToThread(t);
QObject::connect(t,&QThread::started,read,[read,i](){
read->readData(i);
});
t->start();
}
int sum=0;
for(int i=0;i<3;i++){
QThread* t =new QThread;
CacheManger* write = new CacheManger(&m_data,&m_rwLock);
write->moveToThread(t);
QObject::connect(t,&QThread::started,write,[write,&sum](){ //3个线程对缓冲区循环写入数据
write->writeData(sum);
});
t->start();
}
return a.exec();
}
总结
上述展示了Qt多线程编程的基本知识,与C语言、C++等语言线程大体上一致。线程间的同步和互斥涉及到的知识点大体不差。Qt还可以通过信号和槽连接,在不同线程之间进行通信和同步。Qt多线程编程更需要关注的点是线程的生命周期以及如何更加优雅的退出线程。Qt除了线程还提供了一些模块、线程池等功能和机制来实现并发执行。
参考文献:
一文搞定之Qt多线程(QThread、moveToThread)_qthread movetothread-CSDN博客
最后附上源代码链接
对您有帮助的话,帮忙点个star
37-Qmutex-QSempahor · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)
38-QSempahor · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)
39-QWaitCondition · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)
40-QReadWriteLock · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)