Qthread的应用(二)

    QMutexLocer类可以很方便的简化Qmutex的lock和unlock。因为在复杂的函数中Qmutex的锁进行调试代码是很难的,而QMutexLocker在这些情况下使用可以很好的确保mutex的状态。

    下列函数在推出函数时需要解锁的地方有多个,使用QMutex的情况如下,很容易出现忘记解锁的问题,当这个程序的复杂增加时,出现错误的概率也在增大:

 int complexFunction(int flag)
  {
      mutex.lock();

      int retVal = 0;

      switch (flag) {
      case 0:
      case 1:
          retVal = moreComplexFunction(flag);
          break;
      case 2:
          {
              int status = anotherFunction();
              if (status < 0) {
                  mutex.unlock();
                  return -2;
              }
              retVal = status + flag;
          }
          break;
      default:
          if (flag > 10) {
              mutex.unlock();
              return -1;
          }
          break;
      }

      mutex.unlock();
      return retVal;
  }

而下列使用QMutexLocker可以极大的简化代码和增加可读性:

 int complexFunction(int flag)
  {
      QMutexLocker locker(&mutex);

      int retVal = 0;

      switch (flag) {
      case 0:
      case 1:
          return moreComplexFunction(flag);
      case 2:
          {
              int status = anotherFunction();
              if (status < 0)
                  return -2;
              retVal = status + flag;
          }
          break;
      default:
          if (flag > 10)
              return -1;
          break;
      }

      return retVal;
  }

    当QMutexLocker对象被销毁时(即当函数return时),mutex将会unlock.QMutexLocker也提供了一个mutex()的成员函数用于返回QMutexLocking管理的mutex对象。这个函数在需要访问mutex时很有用,比如QWaitCondition::wait()的情况。例如:

class SignalWaiter
  {
  private:
      QMutexLocker locker;

  public:
      SignalWaiter(QMutex *mutex)
          : locker(mutex)
      {
      }

      void waitForSignal()
      {
          ...
          while (!signalled)
              waitCondition.wait(locker.mutex());
          ...
      }
  };

    QT中还提供了QReadLocker和QWriteLocker,用于管理QReadWriteLock,这个和QMutex有些类似,适用于读写线程的保护,这个方法可以实现当一个线程在写文件时,多个线程可以同时读文件,这样可以提高多线程的效率。

3.2 QSemaphore

    QSemaphore是QMutex的一个拓展,可以吧保护一定数量的相同资源。与之相比,QMutex只能保护一个资源。比较常用的例子是同步访问生产者和消费者之间的循环缓存:

1)首先看看QSemaphore例子中的全局变量:

 const int DataSize = 100000;

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

 QSemaphore freeBytes(BufferSize);
 QSemaphore usedBytes;

    DataSize是生产者将生成的数据量。为了使示例尽可能简单,我们将其设为常量。BufferSize是循环缓冲区的大小。它小于DataSize,这意味着在某个时刻,生产者将到达缓冲区的末尾,并从一开始重新启动。

    为了同步生产者和消费者,我们需要两个信号量。freebytes Semaphore控制缓冲区的“空闲”区域(生产者尚未填充数据或消费者已读取的区域)。usedBytes Semaphore控制缓冲区的“已用”区域(生产者已填充但消费者尚未读取的区域)。

    总之,Semaphore确保生产者永远不会超过消费者前面的bufferSize字节,并且消费者永远不会读取生产者尚未生成的数据

    freebytes Semaphore是用bufferSize初始化的,因为最初整个缓冲区是空的。usedBytes Semaphore初始化为0(如果未指定,则为默认值)。

2)生产者

 class Producer : public QThread
  {
  public:
      void run() override
      {
          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();
          }
      }
  };

    生产者生成数据量为DataSize的数据。在将字节写入循环缓冲区之前,必须使用freebytes semaphore获取“free”字节。如果使用者没有跟上生产者的步伐,Qsemaphore::acquire()调用可能会阻塞。

    最后,生产者使用usedbytes semaphore释放一个字节。“空闲”字节已成功转换为“已用”字节,供消费者读取。

3)消费者

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

  signals:
      void stringConsumed(const QString &text);

  protected:
      bool finish;
  };

    消费者的代码与生产者非常相似,与生产者相反,这次我们获取一个“已用”字节并释放一个“空闲”字节。

4)主函数

在主函数中,我们需要调用QT0hread::wait()来确保两个线程完成之后退出:

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

     那么当我们运行程序时会发生什么呢?最初,生产者线程是唯一可以做任何事情的线程;消费者被阻止等待释放usedBytes Semaphore(其初始的available()计数为0)。一旦生产者将一个字节放入缓冲区,freebytes.available()返回bufferSize-1,usedbytes.available()返回1。在这一点上,有两件事可以发生:要么消费者线程接管并读取该字节,要么生产者线程生成第二个字节。

    本例中提供的生产者-消费者模型使编写高度并发的多线程应用程序成为可能。在多处理器机器上,由于两个线程可以同时在缓冲区的不同部分上活动,因此该程序的速度可能是等效的基于QMutex的程序的两倍。

   但要注意,这些好处并不总是能实现的。获取和释放一个Qsemaphore是有成本的。在实践中,将缓冲区划分为块,并对块(而不是单个字节)进行操作是更有价值的。缓冲区大小也是一个必须根据实验仔细选择的参数。

3.3 QWaitCondition

QWaitCondition 同样可以实现上述的生产者消费者模型,并且比单独使用QMutex更有效率。

1)全局变量

const int DataSize = 100000;

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

QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;

    与3.2的QSemaphore的例子相比,为了同步生产者和消费者,我们需要两个QWaitCondition和一个QMutex。当生产者生成了一些数据,BufferNotEmpty QWaitCondition将被发出信号,告诉消费者它可以开始读取数据。当消费者读取了一些数据,BufferNotFull QWaitCondition将被发出信号,并告诉生产者它可以生成更多数据。numusedbytes是缓冲区中包含数据的字节数。

2)生产者

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

      void run() override
      {
          qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

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

              buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];

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

    生产者生成DataSize大小的数据。在向循环缓冲区写入字节之前,必须首先检查缓冲区是否已满(即numusedbytes等于bufferSize)。如果缓冲区已满,线程将在BufferNotFull条件下等待。

    最后,生产者增加numusedbytes,并发出条件BufferNotEmpty为true的信号,因为numusedbytes必须大于0。

    我们用QMutex锁保护对numusedbytes变量的所有访问。此外,QwaitCondition::wait()函数接受QMutex作为其参数。该QMutex在线程进入等待状态之前解锁,在线程唤醒时锁定。此外,从锁定状态到等待状态的转换是原子的,以防止出现争用条件。

3)消费者

 class Consumer : public QThread
  {
      Q_OBJECT
  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");
      }

  signals:
      void stringConsumed(const QString &text);
  };

    类似的,与生产者相比。消费者在读取字节之前,我们检查缓冲区是否为空(numusedbytes为0),代替生产者检查缓冲区是否满;如果缓冲区为空,则等待BufferNotEmpty条件。读取字节后,我们减少numusedbytes(而不是增加它),并向bufferNotFull条件(而不是bufferNotEmpty条件)发送信号。

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;
  }

    同样的,因为QMutex解锁与锁定的成本存在,实际运用中也是将缓存区域划分为为块,并对块进行操作更具实际意义。

四、QMutex常见的应用场景

    在QT的开发过程中,我们很常需要对某个线程中的工作流程进行暂停的操作,这时就可以通过设置一个暂停的PushButton,加上一个QMutex来实现暂停与继续的功能。如下是对应的槽函数

void MainWindow::pause_or_continue()
{
    if(m_state == Running)
    {
        ui->pushButtonpause->setEnabled(false);
        ui->pushButtonpause->setText(QString::fromLocal8Bit("继续"));

        pause_mtx.lock(); //全局锁变量,请求锁

        m_state = Pauseing;
        ui->pushButtonpause->setEnabled(true);
    }

    else  // Pausing
    {
        ui->pushButtonpause->setText(QString::fromLocal8Bit("暂停"));
        m_state = Running;
        pause_mtx.unlock();

    }
}

其中m_state是一个状态变量 ,在构造函数中设置为m_state=Running

enum STATE{Pauseing, Running} m_state;

pause_mtx是一个全局QMutex对象,应用于工作流程线程中资源的保护。通过信号与槽的连接

 connect(ui->pushButtonpause, SIGNAL(clicked(bool)), this, SLOT(pause_or_continue()));

    就可以实现通过QMutex来暂停工作流程线程,点击继续则会释放锁。最后需要注意的是当MainWindow被关闭时,如果此时的m_state=Pauseing,并采用quit()来退出线程时,需要将全局锁pause_mtx解开之后再退出采用quit()退出线程,否则应用程序关闭之后,一直在等待锁的释放,就会出现应用程序的进程无法关闭的问题。

MainWindow::~MainWindow()
{   
    m_worker->stop();
    
    //按下暂停键之后,需要先把锁解除,才能正常关闭工作线程,否则出现进出无法关闭的问题
    if(m_state == Pauseing)
      pause_mtx.unlock();

    workerThread.quit();
    workerThread.wait();
    qDebug()<<("here!");
}

    另外,如果workThread采用terminate()函数终止线程,则锁会被强制终止,但是这样使用terminate()是有风险的,不推荐这样做。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值