当前位置: 首页 > article >正文

QT线程同步

文章目录

  • 前言
  • 1. 使用互斥锁(QMutex)
  • 2.使用QMutexLocker便利类
  • 3. 使用读写锁(QReadWriteLock)
  • 4.QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁
  • 5. 使用信号量(QSemaphore)
  • 6. 使用条件变量(QWaitCondition)
  • 7.使用QThread::wait()
  • 8. 使用事件队列(信号与槽)
  • 9. 使用 QMetaObject::invokeMethod()
  • 总结


前言

多线程编程的一个主要挑战是避免数据竞争和条件竞争。数据竞争发生在多个线程同时读写共享数据,而没有适当的同步机制时。条件竞争是指多个线程以特定的时序执行特定的操作,导致不期望的结果。

同步策略在保护线程安全的同时,也可能会引入额外的性能开销。例如,锁的不当使用可能导致线程争用,从而降低效率。在选择同步策略时,开发者需要权衡以下因素:

  • 锁的粒度:选择合适的锁粒度,以减少争用和上下文切换的开销。
  • 锁的类型:根据应用场景选择互斥锁、读写锁(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::QueuedConnection 确保调用被放入目标线程的事件队列。

总结

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


http://www.kler.cn/a/561582.html

相关文章:

  • 团体程序设计天梯赛-练习集——L1-051 打折
  • 从零开始:OpenCV计算机视觉基础教程【图像基本操作】
  • 量子计算在金融风险评估中的应用:革新与突破
  • [实现Rpc] 测试 | rpc部分功能联调 | debug | 理解bind
  • 【大模型】Ubuntu下 fastgpt 的部署和使用
  • kafka队列堆积的常见解决
  • Linux设备驱动开发-UART驱动
  • Docker打包Python项目
  • 全价值链数字化转型:以美的集团为例,探索开源AI大模型与S2B2C商城小程序源码的融合应用
  • C++ 多态小练习
  • 短视频矩阵系统源码开发/矩阵系统OEM搭建
  • 贪心算法:JAVA从理论到实践的探索
  • Centos主机基础设置和网络网卡设置,安装ansible、docker(修改ip、uuid、主机名、关闭防火墙selinux和networkmanager)
  • [C]基础10.深入理解指针(2)
  • 探寻人工智能的领航之光
  • C++ Primer 算法概述
  • 2025版自动控制流程_工业级连接_智能重连监控系统_增强型工业连接协议 ‘s Vision+Robot EPSON
  • 机器学习数学通关指南——微积分基本概念
  • MacOS 终端选型
  • Visual Studio Code 远程开发方法