前言
多线程编程的一个重要挑战是避免数据竞争和条件竞争。数据竞争发生在多个线程同时读写共享数据,而没有适当的同步机制时。条件竞争是指多个线程以特定的时序执行特定的操作,导致不盼望的效果。
同步策略在保护线程安全的同时,也可能会引入额外的性能开销。例如,锁的不当利用可能导致线程争用,从而降低服从。在选择同步策略时,开辟者需要衡量以下因素:
- 锁的粒度:选择符合的锁粒度,以淘汰争用和上下文切换的开销。
- 锁的范例:根据应用场景选择互斥锁、读写锁(QReadWriteLock)、递归锁(QRecursiveMutex)等。
- 避免死锁:确保代码逻辑上不会出现死锁的环境,死锁会导致程序挂起。
同步策略的精确利用对性能有着决定性影响,需要开辟者仔细设计和测试,
多线程编程的一个重要挑战是避免数据竞争和条件竞争
在Qt中,线程同步是多线程编程中的一个重要环节,用于确保多个线程在访问共享资源时不会发生辩论。Qt提供了多种线程同步方法,包括低级同步原语(如锁)和高级事件队列(如信号与槽机制)。
1. 利用互斥锁(QMutex)
QMutex 是Qt中最基本的同步机制之一,用于保护共享资源,确保同一时刻只有一个线程可以访问该资源。
mutex调用lock后,线程间mutex调用lock会进行阻塞,直到mutex调用unlock解锁才有其他线程调用lock锁住解除阻塞。
示例代码:
- QMutex mutex;
- void SharedResourceAccess() {
- mutex.lock();
- // 访问共享资源的代码
- mutex.unlock();
- }
复制代码 关键点:
lock() 和 unlock() 方法分别用于锁定息争锁。
适用于需要独占访问共享资源的场景。
dialog.h
- #ifndef DIALOG_H
- #define DIALOG_H
- #include <QDialog>
- #include <QMutex>
- #include <QThread>
- namespace Ui {
- class Dialog;
- }
- //第一个线程
- class Thread1 : public QThread
- {
- public:
- void run();//thread1的运行虚函数
- };
- //第二个线程
- class Thread2 : public QThread
- {
- public:
- void run();//thread2的运行虚函数
- };
- class Dialog : public QDialog
- {
- Q_OBJECT
- public:
- explicit Dialog(QWidget *parent = nullptr);
- ~Dialog();
- static void func1();
- static void func2();
- static QMutex mutex;
- private:
- Ui::Dialog *ui;
- Thread1 *thread1;
- Thread2 *thread2;
- static int number; //定义一个资源
- };
- #endif // DIALOG_H
复制代码 dialog.cpp
- #include "dialog.h"
- #include "ui_dialog.h"
- #include <QDebug>
- Dialog::Dialog(QWidget *parent) :
- QDialog(parent),
- ui(new Ui::Dialog)
- {
- ui->setupUi(this);
- //在主线程中实例化子线程
- thread1 = new Thread1;
- thread2 = new Thread2;
- //在主线程中开启子线程的运行
- thread1->start();//开启子线程1的运行
- thread2->start();//开启子线程2的运行
- }
- Dialog::~Dialog()
- {
- delete ui;
- }
- void Dialog::func1()
- {
- mutex.lock();
- qDebug()<<"线程one已经运行";
- number +=50; //number=50
- qDebug()<<"func1中的第一个number="<<number;
- qDebug()<<"thread1已经运行";
- number -=10; //number=40
- qDebug()<<"func1中的第二个number="<<number;
- qDebug()<<"线程1已经运行";
- mutex.unlock();
- }
- QMutex Dialog::mutex; //分配空间
- void Dialog::func2()
- {
- mutex.lock();//如果不加锁的话,会出现两个线程争夺一个资源的情况
- qDebug()<<"线程two已经运行";
- number *=3;
- qDebug()<<"func2中的第一个number="<<number;
- qDebug()<<"线程thread2已经运行";
- number /=2; //number=60
- qDebug()<<"func2中的第二个number="<<number;
- qDebug()<<"线程2已经运行";
- mutex.unlock();
- }
- //两个子线程同时访问同一资源number
- void Thread1::run()//线程1访问资源number
- {
- Dialog::func1();
- Dialog::func2();
- }
- void Thread2::run()//线程2访问资源number
- {
- Dialog::func1();
- Dialog::func2();
- }
复制代码 main.cpp
- #include "dialog.h"
- #include <QApplication>
- int Dialog::number=0;
- int main(int argc, char *argv[])
- {
- QApplication a(argc, argv);
- Dialog w;
- w.show();
- return a.exec();
- }
复制代码 运行效果如下
- 线程one已经运行
- func1中的第一个number= 50
- thread1已经运行
- func1中的第二个number= 40
- 线程1已经运行
- 线程one已经运行
- func1中的第一个number= 90
- thread1已经运行
- func1中的第二个number= 80
- 线程1已经运行
- 线程two已经运行
- func2中的第一个number= 240
- 线程thread2已经运行
- func2中的第二个number= 120
- 线程2已经运行
- 线程two已经运行
- func2中的第一个number= 360
- 线程thread2已经运行
- func2中的第二个number= 180
- 线程2已经运行
复制代码 2.利用QMutexLocker便利类
利用 QMutex 对互斥量进行加锁解锁比较繁琐,在一些复杂的函数大概抛出C++异常的函数中都非常轻易发生错误。可以利用一个方便的 QMutexLocker 类来简化对互斥量的处理。首先,QMutexLocker类的构造函数接收一个QMutex对象作为参数并且上锁,然后在析构函数中自动对其进行解锁。
示例代码:
-
- QMutex mutex;
-
- void someMethod()
- {
- QMutexLocker locker(&mutex);
- qDebug()<<"Hello";
- qDebug()<<"World";
- }
复制代码 这里创建一个QMutexLocker类实例,在这个实例的构造函数中将对mutex对象进行加锁。然后在析构函数中自动对mutex进行解锁。解锁的工作不需要表现地调用unlock函数,而是根据QMutexLocker对象的作用域绑定在一起了。
3. 利用读写锁(QReadWriteLock)
QReadWriteLock 是一种更灵活的锁,允很多个线程同时读取共享资源,但在写操作时会独占访问。它适用于读多写少的场景。
前两种保护互斥量的方法比较绝对,其到达的效果是:不管我要对互斥量做些是什么,都只能我一个人操作,即使我只是看看它,也不能让别人看。这会使得这个互斥量资源的利用率大大下降,造成资源等待等问题。
于是,我们可以对线程对互斥量的操作进行分类:读和写。有几种环境:
1、假如我只是看看的话,你也可以看,大家看到的都是精确的效果;
2、假如我要看这个数据,你是不能改的,不然我看到的效果就不知道是什么了;
3、我在改的时候,你不能看,否则我可能会让你看到不精确的效果;
4、我在改的时候,你固然不能改了。
因此,我们可以对QMutex锁进行升级,将其升级为QReadWriteLock,QMutex加锁的方法是lock(),而QReadWriteLock锁有两种锁法:设置为读锁(lockForRead())和写锁(lockForWrite())。代码如下:
示例代码:
- QReadWriteLock lock;
- void readData() {
- lock.lockForRead();
- // 读取共享资源
- lock.unlock();
- }
- void writeData() {
- lock.lockForWrite();
- // 修改共享资源
- lock.unlock();
- }
复制代码 关键点:
lockForRead() 允很多个线程同时读取。
lockForWrite() 确保写操作独占访问。
于是可能有以下四种环境:
1、一个线程试图对一个加了读锁的互斥量进行上读锁,答应;
2、一个线程试图对一个加了读锁的互斥量进行上写锁,阻塞;
3、一个线程试图对一个加了写锁的互斥量进行上读锁,阻塞;
4、一个线程试图对一个加了写锁的互斥量进行上写锁,阻塞。
所以读写锁比较适用的环境是:对于读写文件的环境。
4.QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁
和QMutex与QMutexLocker类的关系类似,关于读写锁也有两个便利类,读锁和写锁,QReadLocker和QWriteLocker。它们的构造函数都是一个QReadWriteLock对象,差别的是,在QReadLocker的构造函数内里是对读写锁进行lockForRead()加锁操作,而在QWriteLocker的构造函数内里是对读写锁进行lockForWrite()加锁操作。然后解锁操作unlock()都是在析构函数中完成的。
- void write()
- {
- QReadLocker locker(&lock);
- ..........
- }
-
- void read()
- {
- QWriteLocker locker(&lock);
- ..............
- }
复制代码 5. 利用信号量(QSemaphore)
QSemaphore 是一种高级同步机制,用于控制对有限资源的访问。
前面的几种锁都是用来保护只有一个变量的互斥量的。但是还有些互斥量(资源)的数目并不止一个,好比一个电脑安装了2个打印机,我已经申请了一个,但是我不能霸占这两个,你来访问的时候假如发现还有空闲的仍然可以申请到的。于是这个互斥量可以分为两部门,已利用和未利用。一个线程在申请的时候,会对未利用到的部门进行加锁操作,假如加锁失败则阻塞,假如加锁乐成,即又有一个资源被利用了,于是则将已利用到的部门解锁一个。
以著名的生产者消费者问题为例,分析问题:生产者需要的是空闲位置存放产物,效果是可取的产物多了一个。于是,我们可以界说两个信号量:QSemaphore freeSpace和QSemaphore usedSpace,前者是给生产者利用的,后者是给消费者利用的。
示例代码:
- #include <QCoreApplication>
- #include <QSemaphore>
- #include <QDebug>
- #include <QThread>
- int main(int argc, char *argv[])
- {
- QCoreApplication a(argc, argv);
- QSemaphore semaphore(1); // 初始信号量计数为1
- // 创建两个线程,模拟同时访问共享资源
- QThread thread1, thread2;
- QObject::connect(&thread1, &QThread::started, [&]() {
- semaphore.acquire();
- qDebug() << "Thread 1: Accessing shared resource...";
- QThread::sleep(2); // 模拟资源访问
- semaphore.release();
- qDebug() << "Thread 1: Done!";
- });
- QObject::connect(&thread2, &QThread::started, [&]() {
- semaphore.acquire();
- qDebug() << "Thread 2: Accessing shared resource...";
- QThread::sleep(2); // 模拟资源访问
- semaphore.release();
- qDebug() << "Thread 2: Done!";
- });
- thread1.start();
- thread2.start();
- thread1.wait();
- thread2.wait();
- return a.exec();
- }
复制代码 关键点:
acquire() 哀求访问资源。
release() 释放资源。
6. 利用条件变量(QWaitCondition)
QWaitCondition 用于在特定条件下阻塞和唤醒线程。
QWaitCondition::wait() 在利用时必须传入一个上锁的 QMutex 对象。这是很有须要的。
wait() 函数必须传入一个已上锁的 mutex 对象,在 wait() 执行过程中,mutex一直保持上锁状态,直到调用操作体系的wait_block 在阻塞的一刹时把 mutex 解锁(严酷说来应该是原子操作,即体系能包管在真正执行阻塞等待指令时才解锁)。另一线程唤醒后,wait() 函数将在第一时间重新给 mutex 上锁(这种操作也是原子的),直到表现调用 mutex.unlock() 解锁。
返回范例函数名称含义QWaitCondition ()构造函数~QWaitCondition ()析构函数boolwait ( QMutex * mutex, unsigned long time = ULONG_MAX )mutex将被解锁,并且调用线程将会阻塞,直到下列条件之一满意才想来:(1)另一个线程利用wakeOne()或wakeAll()传输给它;(2)time毫秒过去。时boolwait(QMutex *lockedMutex, QDeadlineTimer deadline)同上boolwait ( QReadWriteLock * readWriteLock, unsigned long time = ULONG_MAX )readWriteLock将被解锁,并且调用线程将会阻塞,直到下列条件之一满意才想来:(1)另一个线程利用wakeOne()或wakeAll()传输给它;(2)time毫秒过去。boolwait(QReadWriteLock *lockedReadWriteLock, QDeadlineTimer deadline)同上voidwakeAll ()唤醒全部等待的线程,线程唤醒的顺序不确定,由操作体系的调治策略决定voidwakeOne()唤醒等待QWaitCondition的线程中的一个线程,线程唤醒的顺序不确定,由操作体系的调治策略决定voidnotify_all()同wakeAll()voidnotify_one()同wakeOne() 示例代码:
- // 主线程
- mutex.lock();
- Send(&packet);
- condition.wait(&mutex);
- if (m_receivedPacket)
- {
- HandlePacket(m_receivedPacket); // 另一线程传来回包
- }
- mutex.unlock();
-
-
- // 通信线程
- m_receivedPacket = ParsePacket(buffer); // 将接收的数据解析成包
- mutex.lock();
- condition.wakeAll();
- mutex.unlock();
复制代码 关键点:
wakeOne() 唤醒一个等待的线程。
wait() 阻塞当前线程,直到条件被唤醒。
上述示例二中,主线程先把 mutex 锁占据,即从发送数据包开始,一直到 QWaitCondition::wait() 在操作体系层次真正执行阻塞等待指令,这一段主线程的时间段内,mutex 一直被上锁,即使通信线程很快就接收到数据包,也不会直接调用 wakeAll(),而是在调用 mutex.lock() 时阻塞住(由于主线程已经把mutex占据上锁了,再尝试上锁就会被阻塞),直到主线程 QWaitCondition::wait() 真正执行操作体系的阻塞等待指令并释放mutex,通信线程的 mutex.lock() 才即出阻塞,继续往下执行,调用 wakeAll(),此时肯定能唤醒主线程乐成。
由此可见,通过 mutex 把有严酷时序要求的代码保护起来,同时把 wakeAll() 也用同一个 mutex 保护起来,这样能包管:肯定先有 wait() ,再有 wakeAll(),不管什么环境,都能包管这种先后关系,而不至于摆乌龙。
7.利用QThread::wait()
QThread::wait() 是Qt提供的一个线程同步机制,可以用于等待一个线程完成执行。调用该函数会使当前线程阻塞,直到指定的线程完成执行为止。
以下是一个利用QThread::wait()的示例:
- #include <QThread>
- #include <QDebug>
- class Thread : public QThread
- {
- public:
- void run() override
- {
- qDebug() << "Thread started";
- sleep(1);
- qDebug() << "Thread finished";
- }
- };
- int main(int argc, char *argv[])
- {
- Thread thread;
- thread.start();
- qDebug() << "Waiting for thread to finish...";
- thread.wait();
- qDebug() << "Thread finished, exiting...";
- }
复制代码 8. 利用事件队列(信号与槽)
Qt的事件体系答应通过信号与槽机制在差别线程之间安全通信。
示例代码:
cpp复制
- class Worker : public QObject {
- Q_OBJECT
- public slots:
- void doWork() {
- // 执行耗时操作
- emit workDone();
- }
- signals:
- void workDone();
- };
- QThread workerThread;
- Worker worker;
- worker.moveToThread(&workerThread);
- connect(&workerThread, &QThread::started, &worker, &Worker::doWork);
- connect(&worker, &Worker::workDone, []() {
- qDebug() << "Work done!";
- });
- workerThread.start();
复制代码 关键点:
利用 moveToThread() 将对象移动到目标线程。
利用信号与槽机制进行线程间通信。
9. 利用 QMetaObject::invokeMethod()
QMetaObject::invokeMethod() 可以在差别线程之间调用方法,支持同步和异步调用。
示例代码:
- QThread workerThread;
- Worker worker;
- worker.moveToThread(&workerThread);
- workerThread.start();
- QMetaObject::invokeMethod(&worker, "doWork", Qt::QueuedConnection);
复制代码 关键点:
Qt: ueuedConnection 确保调用被放入目标线程的事件队列。
总结
Qt提供了多种线程同步机制,包括低级锁(如 QMutex 和 QReadWriteLock)、高级同步原语(如 QSemaphore 和 QWaitCondition)以及基于事件队列的通信(如信号与槽和 QMetaObject::invokeMethod())。开辟者可以根据具体需求选择符合的同步方法,以确保多线程程序的安全性和高效性。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |