Qt 多线程同步:互斥锁QMutextLocker 、读写锁 QReadWriteLock、信号量 QSemaphore、 条件变量QWaitConditio、QThread::wait()

1. Qt 多线程为什么需要同步机制 ?

在Qt多线程应用中,如果多个线程同时访问共享资源,可能会导致数据竞争(Data Race)和死锁(Deadlock)等问题。数据竞争会导致程序出现未定义行为,而死锁则会使程序陷入无限等待,无法继续执行。

为了避免这些问题,我们需要使用同步机制来协调多个线程的执行。同步机制可以保证多个线程按照预期的顺序访问共享资源,避免数据竞争和死锁等问题。

2. 多线程有哪些同步机制 ?

Qt多线程提供了互斥锁(Mutex)、信号量(Semaphore)、读写锁(Read-Write Lock)、QWaitCondition、QThread::wait() 等。同步机制可以确保在任何时候只有一个线程能够访问共享资源,从而避免竞争条件的发生

2.1 互斥锁(QMutex)

QMutex类提供的是线程之间的访问顺序化。QMutex的目的是保护一个对象/数据结构或者代码段在同一时间只有一个线程可以访问。基本使用方法如下:

QMutex mutex;
int var;
  
void function()
{
    
    
    mutex.lock();
    // 访问var
    var * var;
    mutex.unlock();
}

如果使用mutex加锁,却没有使用unlock解锁,那么就会造成死锁,其他线程永远也得不到访问变量的机会,所以为了解决这个问题,Qt引入了QMutexLocker类,二者直接可以配合使用更加方便简洁,示例如下:

QMutex mutex;
int var;
 
void function()
{
    
    
    QMutextLocker locker(&mutex); 
    // 访问var
    var * var;
}

QMutexLocker上锁,解锁的原理:在该局部变量被创建的时候上锁,当所在函数运行完毕后该QMutexLocker局部变量在栈中销毁掉,根据他自己的机制也就相对应的解锁了。注意,如果该局部变量在中间被打断,那么QMutexLocker上的锁就不会被解锁掉,因为该函数没有被完整的是执行完。QMutexLocker所创建的局部变量也没有被正确销毁销毁,可能就和QMutexLocker他自己本身的机制不服也就不会解锁。

注意:当一个线程获得了互斥锁时, 其他线程会在尝试获取锁的过程中被阻塞(Blocked),而不是直接退出。当一个线程获取到锁时,其他线程会一直等待,直到该线程释放锁为止。这种等待机制称为阻塞等待(Blocking Wait)。所以尽量减少对共享资源的访问次数,以提高程序的效率。

2.2 读写锁(QReadWriteLock)

QReadWriteLock 是Qt中提供的一种特殊的互斥锁,它可以同时允许多个线程同时读取共享资源,但只能允许一个线程写入共享资源。这种机制的目的是提高多线程程序的效率,特别是在读操作远多于写操作的情况下。 使用示例如下:

int var;
QReadWriteLock lock;
 
void function()
{
    
    
    lock.lockForRead();
    int x = var;
    lock.unlock();
}
 
void function2()
{
    
    
    lock.lockForWrite();
    var = 100;
    lock.unlock();
}

和 QMutexLocker 一样,Qt同样提供了 QReadLocker和QWriteLocker,可自动上锁解锁

int var;
QReadWriteLock lock;
 
void fun()
{
    
    
    QReadLocker(&lock);
    int x = var;
}
 
void fun2()
{
    
    
    QWriteLocker(&lock);
    var = 1000;
}

2.3 信号量(QSemaphore)

QSemaphore可以看作是一个计数器,它维护了一个整数值,表示可用资源的数量。

当一个线程需要获取资源时,它可以通过 acquire() 函数申请资源,如果当前可用资源的数量大于0,则申请成功,QSemaphore 会将可用资源的数量减1,并返回true;否则为0,则申请失败, 则该线程会被阻塞,等待其他线程释放资源。

当一个线程释放资源时,它可以通过 release() 函数释放资源,此时QSemaphore会将可用资源的数量加1,并唤醒一个等待的线程。

QSemaphore是QMutex的一般化,它可以保护一定数量的相同资源,而QMutex只能保护一个资源。信号量比互斥量具有更好的并发性,我们可以利用信号量实现生产者-消费者模式,如下所示:

const int dataSize = 100000;
const int bufferSize = 1024;
char buffer[bufferSize];
QSemaphore freeBytes(bufferSize);   // 可用空间的数量,初始值为1024
QSemaphore usedBtes(0);             // 已使用空间的数量,初始值为0
 
// 生产者线程
void Producer::run()
{
    
    
    for (int i = 0; i < dataSize; ++i)
    {
    
    
        freeBytes.acquire();          //freeBytes 减1
        buffer[i % bufferSize] = i;   
        usedBytes.release();           //usedBytes 加1,  并唤醒等待中的的线程(消费者线程)。
    }
}

// 消费者线程 
void Consumer::run()
{
    
    
    for (int i = 0; i < dataSize; ++i)
    {
    
    
        usedBytes.acquire();                    //usedBytes 减1
        qDebug() << buffer[i % bufferSize];
        freeBytes.release();                    //freeBytes 加1,并唤醒等待中的的线程(生产者线程)。
    }
}

2.4 条件变量 QWaitCondition

同步线程不是通过强制互斥,而是通过提供一个条件变量。其他原语使线程等待资源被解锁,而QWaitCondition使线程等待特定条件被满足。为了让等待的线程继续,可以调用wakeOne()随机唤醒一个线程,或者调用wakeAll()同时唤醒所有线程。

#include <QWaitCondition>
#include <QMutex>
#include <QDebug>

class Thread : public QThread
{
    
    
public:
    Thread(QWaitCondition *condition, QMutex *mutex, int *number)
        : m_condition(condition), m_mutex(mutex), m_number(number)
    {
    
    
    }

    void run() override
    {
    
    
        // 等待条件满足
        m_mutex->lock();
        while (*m_number < 5) {
    
    
            m_condition->wait(m_mutex);
        }
        m_mutex->unlock();

        qDebug() << "Thread finished";
    }

private:
    QWaitCondition *m_condition;
    QMutex *m_mutex;
    int *m_number;
};

int main(int argc, char *argv[])
{
    
    
    QCoreApplication a(argc, argv);

    QWaitCondition condition;
    QMutex mutex;
    int number = 0;

    Thread thread(&condition, &mutex, &number);
    thread.start();

    // 逐渐增加number的值,直到等于5,然后通知线程条件满足
    for (int i = 0; i < 5; ++i) {
    
    
        mutex.lock();
        ++number;
        if (number == 5) {
    
    
            condition.wakeOne();
        }
        mutex.unlock();
    }

    thread.wait();

    return a.exec();
}

在这个示例中,我们创建了一个Thread线程,它等待条件满足后退出。主线程逐渐增加number的值,直到等于5,然后通过调用QWaitCondition的wakeOne()函数通知等待线程条件已经满足。注意,在等待条件之前,线程必须先获取互斥锁,然后再等待条件,这样可以避免多个线程同时等待条件的情况。

需要注意的是,QWaitCondition必须与QMutex一起使用,这是因为等待线程需要在等待条件之前先获取互斥锁,否则可能会出现多个线程同时等待条件的情况。此外,QWaitCondition还提供了wakeAll()函数,可以一次性唤醒所有等待线程。在使用QWaitCondition时,应该根据具体情况选择合适的唤醒方式。

2.5 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[])
{
    
    
    QCoreApplication a(argc, argv);

    Thread thread;
    thread.start();

    qDebug() << "Waiting for thread to finish...";
    thread.wait();

    qDebug() << "Thread finished, exiting...";

    return a.exec();
}

在这个示例中,我们创建了一个Thread线程,并调用start()函数启动它。然后,我们调用QThread::wait()函数等待该线程完成执行。该函数会使当前线程阻塞,直到指定的线程完成执行为止。

注意是当前线程,在上述例子中是 main 中的,因此应该避免在主线程中调用该函数,以免阻塞用户界面。如果需要等待线程完成执行,可以考虑使用信号槽或其他线程同步机制。

猜你喜欢

转载自blog.csdn.net/qq_16504163/article/details/130493328