Qt线程同步的几种方法

Qt线程同步的几种方法

  • 一、QMutex类
  • 二、QMutexLocker便利类
  • 三、QReadWriteLock类
  • 四、QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁
  • 五、信号量QSemaphore
  • 六、条件触发QWaitCondition
    • QWaitCondition 的正确使用方法
    • 简单用法
    • 严谨用法
    • 推而广之
    • 使用Qt编写模块化插件式应用程序

http://www.javashuo.com/article/p-gnyofzbl-gm.html

https://blog.csdn.net/hulifangjiayou/article/details/47018895

QT通过三种形式提供了对线程的支持。它们分别是,一、平台无关的线程类,二、线程安全的事件投递,三、跨线程的信号-槽连接。 这使得开发轻巧的多线程Qt程序更为容易,并能充分利用多处理器机器的优势。多线程编程也是一个有用的模式,它用于解决执行较长时间的操作而不至于用户界面失去响应。在Qt的早期版本中,在构建库时有不选择线程支持的选项,从4.0开始,线程总是有效的。

线程类

Qt 包含下面一些线程相关的类:
QThread 提供了开始一个新线程的方法
QThreadStorage 提供逐线程数据存储
QMutex 提供互斥锁,或互斥量
QMutexLocker 是一个便利类,它可以自动对QMutex加锁与解锁
QReadWriterLock 提供了一个可以同时读操作的锁
QReadLocker与QWriteLocker 是便利类,它自动对QReadWriteLock加锁与解锁
QSemaphore 提供了一个整型信号量,是互斥量的泛化
QWaitCondition 提供了一种方法,使得线程可以在被另外线程唤醒之前一直休眠。

原文链接:https://blog.csdn.net/mao834099514/article/details/51084572

一、QMutex类

QMutex类就像一把锁,在互斥量之前上锁(QMutex::lock()),然后在使用完互斥量之后解锁(QMutex::unlock())。比如下面的代码:

void someMethod()
{
    mutex.lock();
    qDebug()<<"Hello";
    qDebug()<<"World";
    mutex.unlock();
}
 
class Thread1 : public QThread  
{
protected:
    void run()
    {
        someMethod();
    }
};
 
class Thread2 : public QThread  
{
protected:
    void run()
    {
        someMethod();
    }
};

如上面的代码,在函数someMethod里面有两条语句,如果有两个线程启动之后,这两个线程都将调用这个函数(run函数即为线程启动后执行的程序),则可能会出现的结果是Hello Hello World World。但是这并不是我们想要的,我们希望的是每个线程可以一次性执行完someMethod函数里面的代码。这个时候我们便可以在函数俩面给函数体加上锁,然后在结束的时候解锁。
这里需要注意的是,如果一个线程试图向一个已经被其它线程上锁了互斥量上锁的话,这个线程将被阻塞,直到这个互斥量被解锁。如果一个线程希望自己在试图对一个上锁了的互斥量进行访问的时候能够不被阻塞,可以将lock()函数替换为tryLock()函数,这个函数的效果是:如果线程正在试图访问的互斥量已经被上锁了,那么可以立即返回而不被阻塞。

二、QMutexLocker便利类

使用QMutex对互斥量进行加锁解锁比较繁琐,在一些复杂的函数或者抛出C++异常的函数中都非常容易发生错误。可以使用一个方便的QMutexLocker类来简化对互斥量的处理。首先,QMutexLocker类的构造函数接收一个QMutex对象作为参数并且上锁,然后在析构函数中自动对其进行解锁。如下代码:

QMutex mutex;
 
void someMethod()
{
    QMutexLocker locker(&mutex);
    qDebug()<<"Hello";
    qDebug()<<"World";
}

这里创建一个QMutexLocker类实例,在这个实例的构造函数中将对mutex对象进行加锁。然后在析构函数中自动对mutex进行解锁。解锁的工作不需要显示地调用unlock函数,而是根据QMutexLocker对象的作用域绑定在一起了。

在复杂函数或者异常处理中,对mutex进行lock()和unlock()操作将会很复杂,进入点要lock(),在所有跳出点都要unlock(),想想都蛋疼!忘记unlock()将是很苦逼的事情,所以Qt引进了QMutexLocker来避免lock()和unlock()操作。

QMutex mutex;

QMutexLocker locker(&mutex);//在函数需要的地方建立QMutexLocker对象,并把mutex指针传进来就好了,后面什么事情也不用做了,等到退出函数后,这个QMutexLocker对象局部变量会自己销毁,销毁了就解锁了。方便吧。当然,你也可以调用locker.unlock()给mutex解锁,然后再调用locker.relock()再锁住mutex。也可以调用locker.mutex()获取建立locker时引入的那个mutex的指针。是不是很方便?

三、QReadWriteLock类

前两种保护互斥量的方法比较绝对,其达到的效果是:不管我要对互斥量做些是什么,我都要一个人霸占着,即使我只是看看它,也不能让别人看。这会使得这个互斥量资源的使用率大大下降,造成资源等待等问题。于是,我们可以对线程对互斥量的操作进行分类:读和写。有几种情况:
1、如果我只是看看的话,你也可以看,大家看到的都是正确的;
2、如果我要看这个数据,你是不能改的,不然我看到的就不知道是什么了;
3、我在改的时候,你不能看的,我可能会让你看到不正确的了;
4、我在改的时候,你当然不能改了。
因此,我们可以对QMutex锁进行升级,将其升级为QReadWriteLock,QMutex加锁的方法是lock(),而QReadWriteLock锁有两种锁法:设置为读锁(lockForRead())和写锁(lockForWrite())。代码如下:

QReadWriteLock lock;
void someMethod()
{
lock.lockForRead();
//lock.lockForWrite();
qDebug()<<"Hello";
qDebug()<<"World";

lock.unlock();
}

于是可能有一下三种情况:
1、一个线程试图对一个加了读锁的互斥量进行上读锁,允许;
2、一个线程试图对一个加了读锁的互斥量进行上写锁,阻塞;
3、一个线程试图对一个加了写锁的互斥量进行上读锁,阻塞;一个线程试图对一个加了写锁的互斥量进行上写锁,阻塞。
所以可以看出,读写锁比较适用的情况是:需要多次对共享的数据进行读操作的阅读线程。

四、QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁

和QMutex与QMutexLocker类的关系类似,关于读写锁也有两个便利类,读锁和写锁,QReadLocker和QWriteLocker。它们的构造函数都是一个QReadWriteLock对象,不同的是,在QReadLocker的构造函数里面是对读写锁进行lockForRead()加锁操作,而在QWriteLocker的构造函数里面是对读写锁进行lockForWrite()加锁操作。然后解锁操作unlock()都是在析构函数中完成的。

//这个允许多个进程同时读,但是只有一个写。而且写读不能同时进行。

QReadWriteLock lock;

void write()
{
lock.lockForWrite();//为写而锁
..........
lock.unlock();//解锁
}

void read()
{
lock.lockForRead();//为读而锁
..............
lock.unlock();//解锁
}

五、信号量QSemaphore

前面的几种锁都是用来保护只有一个量的互斥量的。但是还有些互斥量(资源)的数量并不止一个,比如一个电脑安装了2个打印机,我已经申请了一个,但是我不能霸占这两个,你来访问的时候如果发现还有空闲的仍然可以申请到的。于是这个互斥量可以分为两部分,已使用和未使用。一个线程在申请的时候,会对未使用到的部分进行加锁操作,如果加锁失败则阻塞,如果加锁成功,即又有一个资源被使用了,于是则将已使用到的部分解锁一个。
以著名的生产者消费者问题为例,分析问题:生产者须要的是空闲位置存放产品,结果是可取的产品多了一个。因而,咱们能够定义两个信号量:QSemaphore freeSpace和QSemaphore usedSpace,前者是给生产者使用的,后者是给消费者使用的。

//它的成员函数是
QSemaphore ( int n = 0 )//建立对象时可以给它n个资源
~QSemaphore ()
void acquire ( int n = 1 )// 这个操作一次减少n个资源,如果现有资源不到n个就会阻塞
int available () const   //返回当前可用的QSemaphore资源个数
void release ( int n = 1 )//这个操作一次增加n个资源
bool tryAcquire ( int n = 1 )//类似于acquire,但是申请不到n个资源时不会阻塞会立即返回
bool tryAcquire ( int n, int timeout )

//下面举生产者-消费者例子说明

 const int DataSize = 100000;//要生产的数据个数
 const int BufferSize = 8192;//用于盛数据的缓存大小
 char buffer[BufferSize];

//要定义两个信号量,一个用于表示自由空间,一个用于表示已用空间
 QSemaphore freeBytes(BufferSize);//自由空间初始化大小当然等于缓存大小啦
 QSemaphore usedBytes;

class Producer : public QThread
 {
 public:
     void run();
 };

 void Producer::run()
 {
     qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
     for (int i = 0; i < DataSize; ++i) {
         freeBytes.acquire();//申请一个自由空间,没有就阻塞
         buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
         usedBytes.release();//已用空间增加一个,有货啦!
     }
 }


class Consumer : public QThread
 {
 public:
     void run();
 };

 void Consumer::run()
 {
     for (int i = 0; i < DataSize; ++i) {
         usedBytes.acquire();
         fprintf(stderr, "%c", buffer[i % BufferSize]);
         freeBytes.release();
     }
     fprintf(stderr, "\n");
 }

int main(int argc, char *argv[])
 {
     QCoreApplication app(argc, argv);
     Producer producer;
     Consumer consumer;
     producer.start();
     consumer.start();
     producer.wait();
     consumer.wait();
     return 0;
 }

六、条件触发QWaitCondition

QWaitCondition最大的好处,我以为,是能在一个线程中唤醒一个或多个其它线程,固然前提是,其它线程在等待某个QWaitCondition,不然不起做用,你唤醒也没用。QWaitCondition必须与QMutex或者QReadwriteLock一块儿用。

QWaitCondition 的正确使用方法

原文链接:https://blog.csdn.net/flyoxs/article/details/54617342

简单用法

QWaitCondition 用于多线程的同步,一个线程调用QWaitCondition::wait() 阻塞等待,直到另一个线程调用QWaitCondition::wake() 唤醒才继续往下执行。

为了描述方便,这里假设主线程调用Send()往通信口发送一个数据包,然后阻塞等待回包才继续往下执行。另一个线程(通信线程)不断从通信口中接收数据并解析成数据包,然后唤醒主线程。下面是按网上给的最简单的方法:

// 示例一

// 主线程
Send(&packet);
mutex.lock();
condition.wait(&mutex); 
if (m_receivedPacket)
{
    HandlePacket(m_receivedPacket); // 另一线程传来回包
}
mutex.unlock();

// 通信线程
m_receivedPacket = ParsePacket(buffer);  // 将接收的数据解析成包
condition.wakeAll();

通常情况下,上述代码能跑得很好。但在某些特殊情况下,可能会出现混乱,大大降低通信可靠性。

在主线程中,调用 Send(&packet) 发送后,假如通信线程立即收到回包,在主线程还来不及调用 wait() 的时候,已经先 wakeAll() 了,显然这次唤醒是无效的,但主线程继续调用 wait(),然后一直阻塞在那里,因为该回的包已经回了。经测试出现这种现象的概率还是挺大的,因为我们不敢保证主线程总会被优先调度。即使主线程已经调用了 wait(),也不能保证底层操作系统的 wait_block 系统调用先于 wake 系统调用,毕竟wait() 函数也是层层封装的。

严谨用法

QWaitCondition::wait() 在使用时必须传入一个上锁的 QMutex 对象。这是很有必要的。而上述示例一代码中,我们虽然用了 mutex,但只是为了形式上传入QMutex参数,让编译器能正常编译而已,事实上,没有其它任何线程再用到这个mutex。而 mutex 本来就是让多个线程能协调工作的,所以上述示例一主线程用的 mutex 是无效的。

根据 Qt 手册,wait() 函数必须传入一个已上锁的 mutex 对象,在 wait() 执行过程中,mutex一直保持上锁状态,直到调用操作系统的wait_block 在阻塞的一瞬间把 mutex 解锁(严格说来应该是原子操作,即系统能保证在真正执行阻塞等待指令时才解锁)。另一线程唤醒后,wait() 函数将在第一时间重新给 mutex 上锁(这种操作也是原子的),直到显示调用 mutex.unlock() 解锁。

在通信线程也用上 mutex 后,整个通信时序正常了,完全解决了示例一的问题。代码如下:

// 示例二

// 主线程
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();

上述示例二中,主线程先把 mutex 锁占据,即从发送数据包开始,一直到 QWaitCondition::wait() 在操作系统层次真正执行阻塞等待指令,这一段主线程的时间段内,mutex 一直被上锁,即使通信线程很快就接收到数据包,也不会直接调用 wakeAll(),而是在调用 mutex.lock() 时阻塞住(因为主线程已经把mutex占据上锁了,再尝试上锁就会被阻塞),直到主线程 QWaitCondition::wait() 真正执行操作系统的阻塞等待指令并释放mutex,通信线程的 mutex.lock() 才即出阻塞,继续往下执行,调用 wakeAll(),此时一定能唤醒主线程成功。

由此可见,通过 mutex 把有严格时序要求的代码保护起来,同时把 wakeAll() 也用同一个 mutex 保护起来,这样能保证:一定先有 wait() ,再有 wakeAll(),不管什么情况,都能保证这种先后关系,而不至于摆乌龙。

推而广之

mutex 和 condition 联合使用是多线程中的一个常用的设计模式,不仅是 Qt,对于 C++ 的 std::condition_variable 和 std::mutex ,以及 java 的 synchronized / wait / notify 也都适用。

使用Qt编写模块化插件式应用程序

https://blog.csdn.net/flyoxs/article/details/5546591

你可能感兴趣的:(Qt经验总结,qt,开发语言)