Qt - 多线程之线程同步
一、线程为什么要同步
线程同步的目的是为了防止多个线程同时访问共享资源时出现数据竞争和不一致的情况。在Qt中,由于GUI操作主要在主线程,其他线程如果需要更新界面或者访问共享数据,就必须进行同步,否则可能导致程序崩溃或者数据错误。
示例:
使用两个线程对一个全局变量做累加,从0加到10,所以只要每个线程累加到5就行。代码如下所示:
#include <QApplication>
#include <QThread>
#include <QDebug>
// 定义共享资源
int sharedValue = 0;
// 定义一个线程类
class MyThread : public QThread
{
public:
    void run() override
    {
        for(int i = 0; i < 5; i++)
        {
            sharedValue++; // 访问共享资源
            qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Shared Value: " << sharedValue;
            msleep(1000); // 线程休眠1秒
        }
    }
};
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyThread thread1;
    MyThread thread2;
    thread1.start();
    thread2.start();
    thread1.wait();
    thread2.wait();
    qDebug() << "Final Shared Value: " << sharedValue;
    return a.exec();
}运行结果:
可以看到两个线程不是交替着执行的,会重复执行,所以并不是我们想要的结果。一般这种问题需要采用线程同步的方式。
结果分析: 明显看出在未加锁情况下对临界资源的访问出现混乱的结果。
二、线程同步方式
Qt提供的同步机制,比如QMutex、QReadWriteLock、QSemaphore,以及信号槽的跨线程通信。需要解释为什么这些机制是必要的,比如当多个线程同时修改同一个变量时,没有同步的话,结果可能无法预测。此外,Qt的信号槽机制虽然可以跨线程,但默认情况下是队列连接,可能需要同步来确保数据安全。
1、QMutex(互斥锁)
互斥锁用于保护共享资源,确保在同一时间只有一个线程能够访问该资源。线程在访问共享资源之前需要获取互斥锁,使用完后再释放互斥锁,以确保同一时间只有一个线程在执行关键代码段。
QMutex的使用步骤如下:
1.创建QMutex对象:在需要进行线程同步的地方,首先创建一个QMutex对象。
QMutex mutex;
2.获取互斥锁:在访问共享资源之前,线程需要获取互斥锁。使用lock()方法获取互斥锁。如果互斥锁已被其他线程占用,当前线程会被阻塞,直到互斥锁被释放。
mutex.lock();
3.访问共享资源:获取到互斥锁后,线程可以安全地访问共享资源。
// 访问共享资源的代码
4.释放互斥锁:在线程完成对共享资源的访问之后,需要释放互斥锁,以便其他线程可以获取到互斥锁进行访问
mutex.unlock();
示例代码:
#include <QApplication>
#include <QThread>
#include <QDebug>
#include <QMutex>
// 定义共享资源
int sharedValue = 0;
// 定义互斥锁
QMutex mutex;
// 定义一个线程类
class MyThread : public QThread
{
public:
    void run() override
    {
        for(int i = 0; i < 5; i++)
        {
            mutex.lock(); // 加锁
            sharedValue++; // 访问共享资源
            qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Shared Value: " << sharedValue;
            msleep(1000); // 线程休眠1秒
            mutex.unlock(); // 解锁
        }
    }
};
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyThread thread1;
    MyThread thread2;
    thread1.start();
    thread2.start();
    thread1.wait();
    thread2.wait();
    qDebug() << "Final Shared Value: " << sharedValue;
    return a.exec();
}运行效果:
2、QReadWriteLock(读写锁)
读写锁是Qt中用于实现读写线程同步的一种机制。它提供了一种更高效的方式来管理对共享资源的读写操作,允许多个线程同时进行读操作,但只允许一个线程进行写操作。
QReadWriteLock的使用步骤如下:
1.创建QReadWriteLock对象:在需要进行读写线程同步的地方,首先创建一个QReadWriteLock对象。
QReadWriteLock rwLock;
2.获取读锁:当线程需要进行读操作时,使用lockForRead()方法获取读锁。多个线程可以同时获取读锁,以进行并发的读操作。
rwLock.lockForRead();
3.进行读操作:获取到读锁后,线程可以安全地进行读操作,访问共享资源
// 进行读操作的代码
4.释放读锁:在读操作完成后,使用unlock()方法释放读锁,允许其他线程获取读锁。
rwLock.unlock();
5.获取写锁:当线程需要进行写操作时,使用lockForWrite()方法获取写锁。写锁是独占的,只允许一个线程获取写锁进行写操作,其他线程需要等待写锁的释放。
rwLock.lockForWrite();
6.进行写操作:获取到写锁后,线程可以安全地进行写操作,修改共享资源。
// 进行写操作的代码
7.释放写锁:在写操作完成后,使用unlock()方法释放写锁,允许其他线程获取读锁或写锁。
rwLock.unlock();
示例代码:
#include <QApplication>
#include <QThread>
#include <QDebug>
#include <QReadWriteLock>
QString sharedData; // 共享数据变量
QReadWriteLock rwLock; // 读写锁
// 读取操作线程
class ReaderThread : public QThread
{
public:
    void run() override
    {
        rwLock.lockForRead(); // 以读取方式加锁
        qDebug() << "Read Data: " << sharedData; // 输出读取的数据
        rwLock.unlock(); // 释放锁
    }
};
// 写入操作线程
class WriterThread : public QThread
{
public:
    void run() override
    {
        rwLock.lockForWrite(); // 以写入方式加锁
        sharedData = "Hello, world!"; // 写入数据
        qDebug() << "Write Data: " << sharedData; // 输出写入的数据
        rwLock.unlock(); // 释放锁
    }
};
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    // 创建读取线程和写入线程
    WriterThread writer;
    ReaderThread reader;
    // 启动线程
    writer.start(); // 启动写入线程
    reader.start(); // 启动读取线程
    // 等待线程结束
    writer.wait(); // 等待写入线程结束
    reader.wait(); // 等待读取线程结束
    return a.exec();
}执行结果:
3、QWaitCondition(条件变量)
QWaitCondition是Qt中用于线程同步的一种机制,它允许线程等待特定条件的发生,并在条件满足时被唤醒继续执行。QWaitCondition通常与QMutex一起使用,以提供更复杂的线程同步机制。
QWaitCondition的使用步骤如下:
1.创建QWaitCondition对象:在需要进行线程同步的地方,首先创建一个QWaitCondition对象。
QWaitCondition condition;
2.创建QMutex对象:为了保护条件的读写操作,创建一个QMutex对象。
QMutex mutex;
3.在等待条件的线程中等待:在线程需要等待特定条件的发生时,使用wait()方法使线程进入等待状态。
mutex.lock(); condition.wait(&mutex); mutex.unlock();
在调用wait()方法之前,需要先获取到互斥锁(QMutex)。这样可以确保线程在等待之前能够安全地访问和检查条件。
4.在其他线程中满足条件并唤醒等待线程:当某个条件满足时,通过wakeOne()或wakeAll()方法唤醒等待的线程。
mutex.lock(); condition.wakeOne(); // 或者使用 condition.wakeAll(); mutex.unlock();
示例代码:
#include <QApplication>
#include <QThread>
#include <QDebug>
#include <QWaitCondition>
#include <QMutex>
#include <QQueue>
QMutex mutex;  // 创建一个互斥锁,确保线程安全
QWaitCondition queueNotEmpty; // 创建一个条件变量,表示队列非空
QQueue<int> queue; // 创建一个队列用于存储数据
// 生产者线程
class ProducerThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i) {
            // 生产数据并加入队列
            {
                QMutexLocker locker(&mutex); // 加锁
                queue.enqueue(i); // 生产数据并加入队列
                qDebug() << "Produced: " << i;
                queueNotEmpty.wakeOne();  // 通知消费者队列非空
            }
            msleep(100);  // 休眠一段时间
        }
    }
};
// 消费者线程
class ConsumerThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i) {
            // 检查队列是否为空,如果为空则等待
            {
                QMutexLocker locker(&mutex); // 加锁
                while (queue.isEmpty()) {
                    queueNotEmpty.wait(&mutex); // 等待条件变量,直到队列非空
                }
                int value = queue.dequeue();  // 从队列中取出数据
                qDebug() << "Consumed: " << value;
            }
            msleep(200);  // 休眠一段时间
        }
    }
};
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    // 创建生产者线程和消费者线程
    ProducerThread producer;
    ConsumerThread consumer;
    // 启动线程
    producer.start();
    consumer.start();
    // 等待线程结束
    producer.wait();
    consumer.wait();
    return a.exec();
}执行结果:
4、QSemaphore(信号量)
QSemaphore 是 Qt 中用于实现信号量的类,用于控制对共享资源的访问数量。它可以用来限制同时访问某一资源的线程数量,也可以用于线程之间的同步。QSemaphore 可以被获取和释放,当信号量的值为正时,线程可以获得该信号量;当信号量的值为零时,线程将被阻塞,直到有线程释放信号量。通过获取和释放信号量,可以实现线程之间的协调和资源的管理。
示例代码:
#include <QApplication>
#include <QThread>
#include <QDebug>
#include <QSemaphore>
QSemaphore semaphore(2); // 定义能够同时访问资源的线程数量为2的信号量
class MyThread : public QThread // 定义一个线程类
{
public:
    void run() override
    {
        if(semaphore.tryAcquire())
        { // 尝试获取信号量
            qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Acquired Semaphore"; // 输出线程ID和已获取信号量消息
            sleep(2); // 线程休眠2秒
            qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Releasing Semaphore"; // 输出线程ID和释放信号量消息
            semaphore.release(); // 释放信号量
        }
        else
        {
            qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Semaphore not acquired"; // 输出线程ID和未获取信号量消息
        }
    }
};
int main(int argc, char *argv[]) // 主函数
{
    QApplication a(argc, argv); // 创建应用程序对象
    MyThread thread1; // 创建线程对象1
    MyThread thread2; // 创建线程对象2
    MyThread thread3; // 创建线程对象3
    thread1.start(); // 启动线程1
    thread2.start(); // 启动线程2
    thread3.start(); // 启动线程3
    thread1.wait(); // 等待线程1结束
    thread2.wait(); // 等待线程2结束
    thread3.wait(); // 等待线程3结束
    return a.exec(); // 执行应用程序事件循环
}执行结果:
当QSemaphore semaphore(2):
当QSemaphore semaphore(3):
三、GUI 线程与非 GUI 线程的交互
- 
Qt 规则:所有 GUI 操作(如更新控件、渲染)必须在主线程执行。 
- 
跨线程通信: - 
使用信号槽( signals/slots)时,Qt 默认通过队列连接(QueuedConnection)自动实现线程安全。
- 
直接调用跨线程方法需手动同步。 
 
- 
// 子线程通过信号槽安全更新 UI
class Worker : public QObject {
    Q_OBJECT
public slots:
    void doWork() {
        // 耗时操作...
        emit resultReady(result);
    }
signals:
    void resultReady(const QString &result);
};
// 主线程连接信号
QObject::connect(worker, &Worker::resultReady, guiThread, &GUI::updateLabel);
================================================
参考文档:https://blog.csdn.net/qq_45790916/article/details/136390241
参考文档:https://www.cnblogs.com/TechNomad/p/17439960.html
================================================







 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号