Qt线程同步之QWaitCondition

1. Qt常用的线程同步类

QMutex,QMutexLocker,QReadWriteLocker,QReadLocker,QWriteLocker,QSemaphore,QWaitCondition

2. QWaitCondition

2.1 概述

QWaitCondition允许线程告诉其他线程某种条件已经满足。一个或多个线程可以阻塞等待QWaitCondition来使用wakeOne()或wakeAll()设置条件。使用wakeOne()唤醒一个随机选择的线程或wakeAll()唤醒所有线程。
QWaitCondition同样可以使多个线程同时访问同一资源,比单纯使用互斥锁的效率更高,类似于QSemaphore。

2.2 成员函数说明

  • QWaitCondition::QWaitCondition()
    创建一个条件变量对象
  • QWaitCondition::~QWaitCondition()
    销毁条件变量对象
  • void QWaitCondition::notify_all()
    这个函数是为了兼容STL而提供的。它相当于wakeAll()。
  • void QWaitCondition::notify_one()
    这个函数是为了兼容STL而提供的。它相当于wakeOne()。
  • bool QWaitCondition::wait(QMutex *lockedMutex, unsigned long time = ULONG_MAX)
    释放lockedMutex并等待条件触发。lockedMutex最初必须由调用线程锁定。如果lockedMutex不处于锁定状态,则该行为未定义。如果lockedMutex是递归互斥,则该函数立即返回。lockedMutex将被解锁,调用线程将阻塞,直到满足以下两个条件之一:
    1.另一个线程使用wakeOne()或wakeAll()向它发出信号。在这种情况下,该函数将返回true。
    2.已经过了 time 毫秒的时间。如果时间是ULONG_MAX(默认值),那么等待永远不会超时(事件必须发出信号)。如果等待超时,此函数将返回false。
    lockedMutex将返回到相同的锁定状态。提供这个函数是为了保证从锁定状态到等待状态的转换是原子的。
  • bool QWaitCondition::wait(QReadWriteLock *lockedReadWriteLock, unsigned long time = ULONG_MAX)
    释放lockedReadWriteLock并等待条件触发。lockedReadWriteLock最初必须由调用线程锁定。如果lockedReadWriteLock没有处于锁定状态,这个函数会立即返回。lockedReadWriteLock不能被递归锁定,否则该函数将不能正确释放锁。lockedReadWriteLock将被解锁,并且调用线程将阻塞,直到满足以下两个条件之一:
    1.另一个线程使用wakeOne()或wakeAll()向它发出信号。在这种情况下,该函数将返回true。
    2.已经过了 time 毫秒的时间。如果时间是ULONG_MAX(默认值),那么等待永远不会超时(事件必须发出信号)。如果等待超时,此函数将返回false。
    lockedReadWriteLock将返回到相同的锁定状态。提供这个函数是为了保证从锁定状态到等待状态的转换是原子的。
  • void QWaitCondition::wakeAll()
    唤醒所有处于等待状态的线程。线程被唤醒的顺序取决于操作系统的调度策略,无法控制或预测。
  • void QWaitCondition::wakeOne()
    唤醒一个在等待条件下的线程。被唤醒的线程取决于操作系统的调度策略,无法控制或预测。
    如果您想唤醒一个特定的线程,解决方案通常是使用不同的等待条件,并让不同的线程在不同的条件下等待。

2.3 代码示例

2.3.1 全局变量

  const int DataSize = 100000;

  const int BufferSize = 8192;
  char buffer[BufferSize];

  QWaitCondition bufferNotEmpty;
  QWaitCondition bufferNotFull;
  QMutex mutex;
  int numUsedBytes = 0;
  • DataSize是生产者将生成的数据量。为了使示例尽可能简单,我们将其设为常数。
  • Buffersize是循环缓冲区的大小。它小于DataSize,这意味着在某个时刻生产者将到达缓冲区的末端,并从开始重新启动。
    为了同步生产者和消费者,我们需要两个等待条件和一个互斥锁。
  • bufferNotEmpty: 当生产者生成一些数据时,就会发出bufferNotEmpty条件信号,告诉消费者可以开始读取数据了。
  • bufferNotFull: 当消费者读取了一些数据时,bufferNotFull条件就会发出信号,告诉生产者它可以生成更多的数据。
  • mutex 使用互斥量保证对线程操作的原子性
  • numUsedBytes是缓冲区中包含数据的字节数,即当前存在多少可用字节。
    条件变量、互斥锁和numUsedBytes计数器一起确保生产者永远不会超过消费者BufferSize字节,并且消费者永远不会读取生产者尚未生成的数据。

2.3.2 生产者类 Producer

  class Producer : public QThread
  {
  public:
      Producer(QObject *parent = NULL) : QThread(parent)
      {
      }

      void run() override
      {
          for (int i = 0; i < DataSize; ++i) {
              mutex.lock();
              if (numUsedBytes == BufferSize)
                  bufferNotFull.wait(&mutex);
              mutex.unlock();

              buffer[i % BufferSize] = "ACGT"[QRandomGenerator::global()->bounded(4)];

              mutex.lock();
              ++numUsedBytes;
              bufferNotEmpty.wakeAll();
              mutex.unlock();
          }
      }
  };

生产者生成DataSize字节的数据。在向循环缓冲区写入字节之前,它必须首先检查缓冲区是否已满(即,numUsedBytes等于BufferSize)。如果缓冲区已满,则线程等待bufferNotFull条件。
最后,生产者增加numUsedBytes,并通知条件bufferNotEmpty为真,因为numUsedBytes必须大于0。
我们用互斥锁保护对numUsedBytes变量的所有访问。另外,QWaitCondition::wait()函数接受一个互斥锁作为它的参数。这个互斥锁在线程进入睡眠状态之前被解锁,在线程醒来时被锁定。此外,从锁定状态到等待状态的转换是原子的,以防止竞态条件的发生。

2.3.3 消费者类 Consumer

  class Consumer : public QThread
  {
  public:
      Consumer(QObject *parent = NULL) : QThread(parent)
      {
      }

      void run() override
      {
          for (int i = 0; i < DataSize; ++i) {
              mutex.lock();
              if (numUsedBytes == 0)
                  bufferNotEmpty.wait(&mutex);
              mutex.unlock();

              fprintf(stderr, "%c", buffer[i % BufferSize]);

              mutex.lock();
              --numUsedBytes;
              bufferNotFull.wakeAll();
              mutex.unlock();
          }
          fprintf(stderr, "\n");
      }
  };

代码与生产者非常相似。在读取字节之前,我们检查缓冲区是否为空(numUsedBytes为0),而不是缓冲区是否已满,如果缓冲区为空,则等待bufferNotEmpty条件。在读取字节之后,我们减少numUsedBytes(而不是增加它),并发出bufferNotFull条件的信号(而不是bufferNotEmpty条件)。

2.3.4 主函数

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

在main()中,我们创建了两个线程,并调用QThread::wait()来确保两个线程在我们退出之前都有时间完成。
那么当我们运行程序时会发生什么呢?最初,生产者线程是唯一可以做任何事情的线程;消费者被阻塞,等待bufferNotEmpty条件被告知(numUsedBytes不为0)。一旦生产者在缓冲区中放入一个字节,numUsedBytes的剩余空间为BufferSize - 1, bufferNotEmpty条件被告知。此时,可能会发生两件事:要么消费者线程接管并读取该字节,要么生产者生成第二个字节。
本例中提供的生产者-消费者模型使得编写高度并发的多线程应用程序成为可能。在多处理器机器上,该程序的速度可能是等效的基于互斥锁的程序的两倍,因为两个线程可以在缓冲区的不同部分同时处于活动状态。
但要意识到,这些好处并不总能实现。锁定和解锁QMutex是有代价的。在实践中,将缓冲区划分为块并对块操作,而不是对单个字节进行操作是更好的处理方法。缓冲区大小也是一个必须根据实际情况仔细选择的参数。

说明来源qt官方文档
代码来源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VectorAL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值