QT线程同步

打印 上一主题 下一主题

主题 871|帖子 871|积分 2613


前言

多线程编程的一个重要挑战是避免数据竞争和条件竞争。数据竞争发生在多个线程同时读写共享数据,而没有适当的同步机制时。条件竞争是指多个线程以特定的时序执行特定的操作,导致不盼望的效果。
同步策略在保护线程安全的同时,也可能会引入额外的性能开销。例如,锁的不当利用可能导致线程争用,从而降低服从。在选择同步策略时,开辟者需要衡量以下因素:


  • 锁的粒度:选择符合的锁粒度,以淘汰争用和上下文切换的开销。
  • 锁的范例:根据应用场景选择互斥锁、读写锁(QReadWriteLock)、递归锁(QRecursiveMutex)等。
  • 避免死锁:确保代码逻辑上不会出现死锁的环境,死锁会导致程序挂起。
    同步策略的精确利用对性能有着决定性影响,需要开辟者仔细设计和测试,
多线程编程的一个重要挑战是避免数据竞争和条件竞争
在Qt中,线程同步是多线程编程中的一个重要环节,用于确保多个线程在访问共享资源时不会发生辩论。Qt提供了多种线程同步方法,包括低级同步原语(如锁)和高级事件队列(如信号与槽机制)。
1. 利用互斥锁(QMutex)

QMutex 是Qt中最基本的同步机制之一,用于保护共享资源,确保同一时刻只有一个线程可以访问该资源。
mutex调用lock后,线程间mutex调用lock会进行阻塞,直到mutex调用unlock解锁才有其他线程调用lock锁住解除阻塞。
示例代码:
  1. QMutex mutex;
  2. void SharedResourceAccess() {
  3.     mutex.lock();
  4.     // 访问共享资源的代码
  5.     mutex.unlock();
  6. }
复制代码
关键点:
lock() 和 unlock() 方法分别用于锁定息争锁。
适用于需要独占访问共享资源的场景。
dialog.h
  1. #ifndef DIALOG_H
  2. #define DIALOG_H
  3. #include <QDialog>
  4. #include <QMutex>
  5. #include <QThread>
  6. namespace Ui {
  7. class Dialog;
  8. }
  9. //第一个线程
  10. class Thread1 : public QThread
  11. {
  12. public:
  13.     void run();//thread1的运行虚函数
  14. };
  15. //第二个线程
  16. class Thread2 : public QThread
  17. {
  18. public:
  19.     void run();//thread2的运行虚函数
  20. };
  21. class Dialog : public QDialog
  22. {
  23.     Q_OBJECT
  24. public:
  25.     explicit Dialog(QWidget *parent = nullptr);
  26.     ~Dialog();
  27.     static void func1();
  28.     static void func2();
  29.     static QMutex mutex;
  30. private:
  31.     Ui::Dialog *ui;
  32.     Thread1 *thread1;
  33.     Thread2 *thread2;
  34.     static int number; //定义一个资源
  35. };
  36. #endif // DIALOG_H
复制代码
dialog.cpp
  1. #include "dialog.h"
  2. #include "ui_dialog.h"
  3. #include <QDebug>
  4. Dialog::Dialog(QWidget *parent) :
  5.     QDialog(parent),
  6.     ui(new Ui::Dialog)
  7. {
  8.     ui->setupUi(this);
  9.     //在主线程中实例化子线程
  10.     thread1 = new Thread1;
  11.     thread2 = new Thread2;
  12.     //在主线程中开启子线程的运行
  13.     thread1->start();//开启子线程1的运行
  14.     thread2->start();//开启子线程2的运行
  15. }
  16. Dialog::~Dialog()
  17. {
  18.     delete ui;
  19. }
  20. void Dialog::func1()
  21. {
  22.     mutex.lock();
  23.     qDebug()<<"线程one已经运行";
  24.     number +=50; //number=50
  25.     qDebug()<<"func1中的第一个number="<<number;
  26.     qDebug()<<"thread1已经运行";
  27.     number -=10; //number=40
  28.     qDebug()<<"func1中的第二个number="<<number;
  29.     qDebug()<<"线程1已经运行";
  30.     mutex.unlock();
  31. }
  32. QMutex Dialog::mutex; //分配空间
  33. void Dialog::func2()
  34. {
  35.     mutex.lock();//如果不加锁的话,会出现两个线程争夺一个资源的情况
  36.     qDebug()<<"线程two已经运行";
  37.     number *=3;
  38.     qDebug()<<"func2中的第一个number="<<number;
  39.     qDebug()<<"线程thread2已经运行";
  40.     number /=2; //number=60
  41.     qDebug()<<"func2中的第二个number="<<number;
  42.     qDebug()<<"线程2已经运行";
  43.     mutex.unlock();
  44. }
  45. //两个子线程同时访问同一资源number
  46. void Thread1::run()//线程1访问资源number
  47. {
  48.     Dialog::func1();
  49.     Dialog::func2();
  50. }
  51. void Thread2::run()//线程2访问资源number
  52. {
  53.     Dialog::func1();
  54.     Dialog::func2();
  55. }
复制代码
main.cpp
  1. #include "dialog.h"
  2. #include <QApplication>
  3. int Dialog::number=0;
  4. int main(int argc, char *argv[])
  5. {
  6.     QApplication a(argc, argv);
  7.     Dialog w;
  8.     w.show();
  9.     return a.exec();
  10. }
复制代码
运行效果如下
  1. 线程one已经运行
  2. func1中的第一个number= 50
  3. thread1已经运行
  4. func1中的第二个number= 40
  5. 线程1已经运行
  6. 线程one已经运行
  7. func1中的第一个number= 90
  8. thread1已经运行
  9. func1中的第二个number= 80
  10. 线程1已经运行
  11. 线程two已经运行
  12. func2中的第一个number= 240
  13. 线程thread2已经运行
  14. func2中的第二个number= 120
  15. 线程2已经运行
  16. 线程two已经运行
  17. func2中的第一个number= 360
  18. 线程thread2已经运行
  19. func2中的第二个number= 180
  20. 线程2已经运行
复制代码
2.利用QMutexLocker便利类

利用 QMutex 对互斥量进行加锁解锁比较繁琐,在一些复杂的函数大概抛出C++异常的函数中都非常轻易发生错误。可以利用一个方便的 QMutexLocker 类来简化对互斥量的处理。首先,QMutexLocker类的构造函数接收一个QMutex对象作为参数并且上锁,然后在析构函数中自动对其进行解锁。
示例代码:
  1. QMutex mutex;
  2. void someMethod()
  3. {
  4.     QMutexLocker locker(&mutex);
  5.     qDebug()<<"Hello";
  6.     qDebug()<<"World";
  7. }
复制代码
这里创建一个QMutexLocker类实例,在这个实例的构造函数中将对mutex对象进行加锁。然后在析构函数中自动对mutex进行解锁。解锁的工作不需要表现地调用unlock函数,而是根据QMutexLocker对象的作用域绑定在一起了。
3. 利用读写锁(QReadWriteLock)

QReadWriteLock 是一种更灵活的锁,允很多个线程同时读取共享资源,但在写操作时会独占访问。它适用于读多写少的场景。
前两种保护互斥量的方法比较绝对,其到达的效果是:不管我要对互斥量做些是什么,都只能我一个人操作,即使我只是看看它,也不能让别人看。这会使得这个互斥量资源的利用率大大下降,造成资源等待等问题。
于是,我们可以对线程对互斥量的操作进行分类:读和写。有几种环境:
1、假如我只是看看的话,你也可以看,大家看到的都是精确的效果;
2、假如我要看这个数据,你是不能改的,不然我看到的效果就不知道是什么了;
3、我在改的时候,你不能看,否则我可能会让你看到不精确的效果;
4、我在改的时候,你固然不能改了。
因此,我们可以对QMutex锁进行升级,将其升级为QReadWriteLock,QMutex加锁的方法是lock(),而QReadWriteLock锁有两种锁法:设置为读锁(lockForRead())和写锁(lockForWrite())。代码如下:
示例代码:
  1. QReadWriteLock lock;
  2. void readData() {
  3.     lock.lockForRead();
  4.     // 读取共享资源
  5.     lock.unlock();
  6. }
  7. void writeData() {
  8.     lock.lockForWrite();
  9.     // 修改共享资源
  10.     lock.unlock();
  11. }
复制代码
关键点:
lockForRead() 允很多个线程同时读取。
lockForWrite() 确保写操作独占访问。
于是可能有以下四种环境:
1、一个线程试图对一个加了读锁的互斥量进行上读锁,答应;
2、一个线程试图对一个加了读锁的互斥量进行上写锁,阻塞;
3、一个线程试图对一个加了写锁的互斥量进行上读锁,阻塞;
4、一个线程试图对一个加了写锁的互斥量进行上写锁,阻塞。
所以读写锁比较适用的环境是:对于读写文件的环境。
4.QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁

和QMutex与QMutexLocker类的关系类似,关于读写锁也有两个便利类,读锁和写锁,QReadLocker和QWriteLocker。它们的构造函数都是一个QReadWriteLock对象,差别的是,在QReadLocker的构造函数内里是对读写锁进行lockForRead()加锁操作,而在QWriteLocker的构造函数内里是对读写锁进行lockForWrite()加锁操作。然后解锁操作unlock()都是在析构函数中完成的。
  1. void write()
  2. {
  3. QReadLocker locker(&lock);
  4. ..........
  5. }
  6. void read()
  7. {
  8. QWriteLocker locker(&lock);
  9. ..............
  10. }
复制代码
5. 利用信号量(QSemaphore)

QSemaphore 是一种高级同步机制,用于控制对有限资源的访问。
前面的几种锁都是用来保护只有一个变量的互斥量的。但是还有些互斥量(资源)的数目并不止一个,好比一个电脑安装了2个打印机,我已经申请了一个,但是我不能霸占这两个,你来访问的时候假如发现还有空闲的仍然可以申请到的。于是这个互斥量可以分为两部门,已利用和未利用。一个线程在申请的时候,会对未利用到的部门进行加锁操作,假如加锁失败则阻塞,假如加锁乐成,即又有一个资源被利用了,于是则将已利用到的部门解锁一个。
以著名的生产者消费者问题为例,分析问题:生产者需要的是空闲位置存放产物,效果是可取的产物多了一个。于是,我们可以界说两个信号量:QSemaphore freeSpace和QSemaphore usedSpace,前者是给生产者利用的,后者是给消费者利用的。
示例代码:
  1. #include <QCoreApplication>
  2. #include <QSemaphore>
  3. #include <QDebug>
  4. #include <QThread>
  5. int main(int argc, char *argv[])
  6. {
  7.     QCoreApplication a(argc, argv);
  8.     QSemaphore semaphore(1); // 初始信号量计数为1
  9.     // 创建两个线程,模拟同时访问共享资源
  10.     QThread thread1, thread2;
  11.     QObject::connect(&thread1, &QThread::started, [&]() {
  12.         semaphore.acquire();
  13.         qDebug() << "Thread 1: Accessing shared resource...";
  14.         QThread::sleep(2); // 模拟资源访问
  15.         semaphore.release();
  16.         qDebug() << "Thread 1: Done!";
  17.     });
  18.     QObject::connect(&thread2, &QThread::started, [&]() {
  19.         semaphore.acquire();
  20.         qDebug() << "Thread 2: Accessing shared resource...";
  21.         QThread::sleep(2); // 模拟资源访问
  22.         semaphore.release();
  23.         qDebug() << "Thread 2: Done!";
  24.     });
  25.     thread1.start();
  26.     thread2.start();
  27.     thread1.wait();
  28.     thread2.wait();
  29.     return a.exec();
  30. }
复制代码
关键点:
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() 示例代码:
  1. // 主线程
  2. mutex.lock();
  3. Send(&packet);
  4. condition.wait(&mutex);
  5. if (m_receivedPacket)
  6. {
  7.     HandlePacket(m_receivedPacket); // 另一线程传来回包
  8. }
  9. mutex.unlock();
  10. // 通信线程
  11. m_receivedPacket = ParsePacket(buffer);  // 将接收的数据解析成包
  12. mutex.lock();
  13. condition.wakeAll();
  14. 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()的示例:
  1. #include <QThread>
  2. #include <QDebug>
  3. class Thread : public QThread
  4. {
  5. public:
  6.     void run() override
  7.     {
  8.         qDebug() << "Thread started";
  9.         sleep(1);
  10.         qDebug() << "Thread finished";
  11.     }
  12. };
  13. int main(int argc, char *argv[])
  14. {
  15.     Thread thread;
  16.     thread.start();
  17.     qDebug() << "Waiting for thread to finish...";
  18.     thread.wait();
  19.     qDebug() << "Thread finished, exiting...";
  20. }
复制代码
8. 利用事件队列(信号与槽)

Qt的事件体系答应通过信号与槽机制在差别线程之间安全通信。
示例代码:
cpp复制
  1. class Worker : public QObject {
  2.     Q_OBJECT
  3. public slots:
  4.     void doWork() {
  5.         // 执行耗时操作
  6.         emit workDone();
  7.     }
  8. signals:
  9.     void workDone();
  10. };
  11. QThread workerThread;
  12. Worker worker;
  13. worker.moveToThread(&workerThread);
  14. connect(&workerThread, &QThread::started, &worker, &Worker::doWork);
  15. connect(&worker, &Worker::workDone, []() {
  16.     qDebug() << "Work done!";
  17. });
  18. workerThread.start();
复制代码
关键点:
利用 moveToThread() 将对象移动到目标线程。
利用信号与槽机制进行线程间通信。
9. 利用 QMetaObject::invokeMethod()

QMetaObject::invokeMethod() 可以在差别线程之间调用方法,支持同步和异步调用。
示例代码:
  1. QThread workerThread;
  2. Worker worker;
  3. worker.moveToThread(&workerThread);
  4. workerThread.start();
  5. QMetaObject::invokeMethod(&worker, "doWork", Qt::QueuedConnection);
复制代码
关键点:
Qt:ueuedConnection 确保调用被放入目标线程的事件队列。
总结

Qt提供了多种线程同步机制,包括低级锁(如 QMutex 和 QReadWriteLock)、高级同步原语(如 QSemaphore 和 QWaitCondition)以及基于事件队列的通信(如信号与槽和 QMetaObject::invokeMethod())。开辟者可以根据具体需求选择符合的同步方法,以确保多线程程序的安全性和高效性。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

农民

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表