简述

为了让程序尽快响应用户操作,在开发应用程序时经常会使用到线程。对于耗时操作如果不使用线程,UI界面将会长时间处于停滞状态,这种情况是用户非常不愿意看到的,我们可以用线程来解决这个问题。

前面,已经介绍了QThread常用的两种方式:

  • Worker-Object
  • 子类化QThread

下面,我们来看看子类化QThread在日常中的应用。

大多数情况下,多线程耗时操作会与UI进行交互,比如:显示进度、加载等待。。。让用户明确知道目前的状态,并对结果有一个直观的预期,甚至有趣巧妙的设计,能让用户爱上等待,把等待看成一件很美好的事。

子类化QThread

下面,是一个使用多线程操作UI界面的示例 - 更新进度条。与此同时,分享在此过程中有可能遇到的问题及解决方法。

定义一个WorkerThread类,让其继承自QThread,并重写run()函数,每隔50毫秒更新当前值,然后发射resultReady()信号(用于更新进度条)。

#include <QThread>

class WorkerThread : public QThread
{
Q_OBJECT public:
explicit WorkerThread(QObject *parent = 0)
: QThread(parent)
{
qDebug() << "Worker Thread : " << QThread::currentThreadId();
} protected:
virtual void run() Q_DECL_OVERRIDE {
qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
int nValue = 0;
while (nValue < 100)
{
// 休眠50毫秒
msleep(50);
++nValue; // 准备更新
emit resultReady(nValue);
}
}
signals:
void resultReady(int value);
};

构建一个主界面 - 包含按钮、进度条,当点击“开始”按钮时,启动线程,更新进度条。

class MainWindow : public CustomWindow
{
Q_OBJECT public:
explicit MainWindow(QWidget *parent = 0)
: CustomWindow(parent)
{
qDebug() << "Main Thread : " << QThread::currentThreadId(); // 创建开始按钮、进度条
QPushButton *pStartButton = new QPushButton(this);
m_pProgressBar = new QProgressBar(this); //设置文本、进度条取值范围
pStartButton->setText(QString::fromLocal8Bit("开始"));
m_pProgressBar->setFixedHeight(25);
m_pProgressBar->setRange(0, 100);
m_pProgressBar->setValue(0); QVBoxLayout *pLayout = new QVBoxLayout();
pLayout->addWidget(pStartButton, 0, Qt::AlignHCenter);
pLayout->addWidget(m_pProgressBar);
pLayout->setSpacing(50);
pLayout->setContentsMargins(10, 10, 10, 10);
setLayout(pLayout); // 连接信号槽
connect(pStartButton, SIGNAL(clicked(bool)), this, SLOT(startThread()));
} ~MainWindow(){} private slots:
// 更新进度
void handleResults(int value)
{
qDebug() << "Handle Thread : " << QThread::currentThreadId();
m_pProgressBar->setValue(value);
} // 开启线程
void startThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
// 线程结束后,自动销毁
connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater()));
workerThread->start();
} private:
QProgressBar *m_pProgressBar;
WorkerThread m_workerThread;
};

显然,UI界面、Worker构造函数、槽函数处于同一线程(主线程),而run()函数处于另一线程(次线程)。

Main Thread : 0x34fc

Worker Thread : 0x34fc

Worker Run Thread : 0x4038

Handle Thread : 0x34fc

由于信号与槽连接类型默认为“Qt::AutoConnection”,在这里相当于“Qt::QueuedConnection”。

也就是说,槽函数在接收者的线程(主线程)中执行。

注意:信号与槽的连接类型,请参考:Qt之Threads和QObjects中“跨线程的信号和槽”部分。

线程休眠

上述示例中,通过在run()函数中调用msleep(50),线程会每隔50毫秒让当前的进度值加1,然后发射一个resultReady()信号,其余时间什么都不做。在这段空闲时间,线程不占用任何的系统资源。当下一次CPU时钟来临时,它会继续执行。

QThread提供了静态的、平台独立的休眠函数:sleep()、msleep()、usleep(),允许秒,毫秒和微秒来区分,函数接受整型数值作为参数,以表明线程挂起执行的时间。当休眠时间结束,线程就会获得CPU时钟,将继续执行它的指令。

想象一下,日常用的电脑,如果我们需要离开一段时间,可以将它设置为休眠状态,为了节约用电,同时响应国家政策 - 走绿色、环保之道。

可以尝试注释掉休眠部分的代码,这时,由于没有任何耗时操作,会造成频繁地更新UI。所以,为了保证界面的流畅性,同时确保进度的更新在人眼可接受的范围内,我们应在必要的时候加上适当时间的休眠。

在主线程中更新UI

当连接方式更改为“Qt::DirectConnection”时:

connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)), Qt::DirectConnection);

再次点击“开始”按钮,会很失望,因为它会出现一个异常,描述如下:

ASSERT failure in QCoreApplication::sendEvent: “Cannot send events to objects owned by a different thread. Current thread e346e8. Receiver customWidget’ (of type ‘MainWindow’) was created in thread 4186a0”, file kernel\qcoreapplication.cpp, line 553

显然,UI界面、Worker构造函数处于同一线程(主线程),而run()函数、槽函数处于同一线程(次线程)。

Main Thread : 0x2c6c

Worker Thread : 0x2c6c

Worker Run Thread : 0x4704

Handle Thread : 0x4704

之所以会出现这种情况是因为Qt做了限制(其它大多数GUI编程也一样),不允许在其它线程(非主线程)中访问UI控件,这么做主要是怕在多线程环境下对界面控件进行操作会出现不可预知的情况。

所以,不难理解,由于在槽函数(次线程)中更新了UI,所以,会引起以上错误。

避免多次connect

当多次点击“开始”按钮的时候,就会多次connect(),从而启动多个线程,同时更新进度条。

为了避免这个问题,我们修改如下:

class MainWindow : public CustomWindow
{
Q_OBJECT public:
explicit MainWindow(QWidget *parent = 0)
: CustomWindow(parent)
{
// ...
connect(&m_workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
} ~MainWindow(){} private slots:
// ...
void startThread()
{
if (!m_workerThread.isRunning())
m_workerThread.start();
} private:
WorkerThread m_workerThread;
};

将connect添加在构造函数中,保证了信号槽的正常连接。在线程start()之前,可以使用isFinished()和isRunning()来查询线程的状态,判断线程是否正在运行,以确保线程的正常启动。

优雅地结束线程

如果一个线程运行完成,就会结束。可很多情况并非这么简单,由于某种特殊原因,当线程还未执行完时,我们就想中止它。

不恰当的中止往往会引起一些未知错误。比如:当关闭主界面的时候,很有可能次线程正在运行,这时,就会出现如下提示:

QThread: Destroyed while thread is still running

这是因为次线程还在运行,就结束了UI主线程,导致事件循环结束。这个问题在使用线程的过程中经常遇到,尤其是耗时操作。

在此问题上,常见的两种人:

  • 直接忽略此问题。
  • 强制中止 - terminate()。

大多数情况下,当程序退出时,次线程也许会正常退出。这时,虽然抱着侥幸心理,但隐患依然存在,也许在极少数情况下,就会出现Crash。

正如前面提到过terminate(),比较危险,不鼓励使用。线程可以在代码执行的任何点被终止。线程可能在更新数据时被终止,从而没有机会来清理自己,解锁等等。。。总之,只有在绝对必要时使用此函数。

举一个简单的例子:当你的哥们正处于长时间的酣睡状态时,你想要叫醒他,但是采取的措施却是泼一盆凉水,想象一下后果?这凉爽 - O(∩_∩)O哈哈~。

所以,我们应该采取合理的措施来优雅地结束线程,一般思路:

  1. 发起线程退出操作,调用quit()或exit()。
  2. 等待线程完全停止,删除创建在堆上的对象。
  3. 适当的使用wait()(用于等待线程的退出)和合理的算法。

下面介绍两种方式:

  • QMutex互斥锁 + bool成员变量。

这种方式是Qt4.x中比较常用的,主要是利用“QMutex互斥锁 + bool成员变量”的方式来保证共享数据的安全性(可以完全参照下面的requestInterruption()源码写法)。

#include <QThread>
#include <QMutexLocker> class WorkerThread : public QThread
{
Q_OBJECT public:
explicit WorkerThread(QObject *parent = 0)
: QThread(parent),
m_bStopped(false)
{
qDebug() << "Worker Thread : " << QThread::currentThreadId();
} ~WorkerThread()
{
stop();
quit();
wait();
} void stop()
{
qDebug() << "Worker Stop Thread : " << QThread::currentThreadId();
QMutexLocker locker(&m_mutex);
m_bStopped = true;
} protected:
virtual void run() Q_DECL_OVERRIDE {
qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
int nValue = 0;
while (nValue < 100)
{
// 休眠50毫秒
msleep(50);
++nValue; // 准备更新
emit resultReady(nValue); // 检测是否停止
{
QMutexLocker locker(&m_mutex);
if (m_bStopped)
break;
}
// locker超出范围并释放互斥锁
}
}
signals:
void resultReady(int value); private:
bool m_bStopped;
QMutex m_mutex;
};

为什么要加锁?很简单,是为了共享数据段操作的互斥。

何时需要加锁?在形成资源竞争的时候,也就是说,多个线程有可能访问同一共享资源的时候。

当主线程调用stop()更新m_bStopped的时候,run()函数也极有可能正在访问它(这时,他们处于不同的线程),所以存在资源竞争,因此需要加锁,保证共享数据的安全性。

  • Qt5以后:requestInterruption() + isInterruptionRequested()

这两个接口是Qt5.x引入的,使用很方便:

class WorkerThread : public QThread
{
Q_OBJECT public:
explicit WorkerThread(QObject *parent = 0)
: QThread(parent)
{
} ~WorkerThread() {
// 请求终止
requestInterruption();
quit();
wait();
} protected:
virtual void run() Q_DECL_OVERRIDE {
// 是否请求终止
while (!isInterruptionRequested())
{
// 耗时操作
}
}
};

在耗时操作中使用isInterruptionRequested()来判断是否请求终止线程,如果没有,则一直运行;当希望终止线程的时候,调用requestInterruption()即可。

正如侯捷所言:「源码面前,了无秘密」。如果还心存疑虑,我们不妨来看看requestInterruption()、isInterruptionRequested()的源码:

void QThread::requestInterruption()
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (!d->running || d->finished || d->isInFinish)
return;
if (this == QCoreApplicationPrivate::theMainThread) {
qWarning("QThread::requestInterruption has no effect on the main thread");
return;
}
d->interruptionRequested = true;
} bool QThread::isInterruptionRequested() const
{
Q_D(const QThread);
QMutexLocker locker(&d->mutex);
if (!d->running || d->finished || d->isInFinish)
return false;
return d->interruptionRequested;
}

^_^,内部实现居然也用了互斥锁QMutex,这样我们就可以放心地使用了。

更多参考

Qt之QThread(深入理解)的更多相关文章

  1. 解析Qt中QThread使用方法

    本文讲述的是在Qt中QThread使用方法,QThread似乎是很难的一个东西,特别是信号和槽,有非常多的人(尽管使用者本人往往不知道)在用不恰当(甚至错误)的方式在使用QThread,随便用goog ...

  2. Qt线程QThread简析(8个线程等级,在UI线程里可调用thread->wait()等待线程结束,exit()可直接退出线程,setStackSize设置线程堆栈,首次见到Qt::HANDLE,QThreadData和QThreadPrivate)

    QThread实例代表一个线程,我们可以重新实现QThread::run(),要新建一个线程,我们应该先继承QThread并重新实现run()函数. 需要注意的是: 1.必须在创建QThread对象之 ...

  3. Qt多线程-QThread

    版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:Qt多线程-QThread     本文地址:http://techieliang.com/2 ...

  4. Qt之QThread随记

    这是一篇随记,排版什么的就没有那么好了:) 首先要知道,一个线程在资源分配完之后是以某段代码为起点开始执行的,例如STL内的std::thread,POSIX下的pthread等,都是以函数加其参数之 ...

  5. Qt线程—QThread的使用--run和movetoThread的用法

    Qt使用线程主要有两种方法: 方法一:继承QThread,重写run()的方法 QThread是一个非常便利的跨平台的对平台原生线程的抽象.启动一个线程是很简单的.让我们看一个简短的代码:生成一个在线 ...

  6. QT下QThread学习(二)

    学习QThread主要是为了仿照VC下的FTP服务器写个QT版本.不多说,上图. FTP服务器的软件结构在上面的分析中就已经解释了,今天要解决的就是让每一个客户端的处理过程都可以按一个线程来单独跑.先 ...

  7. Qt之QThread

    简述 QThread类提供了与系统无关的线程. QThread代表在程序中一个单独的线程控制.线程在run()中开始执行,默认情况下,run()通过调用exec()启动事件循环并在线程里运行一个Qt的 ...

  8. Qt之Q_PROPERTY宏理解

    在初学Qt的过程中,时不时地要通过F2快捷键来查看QT类的定义,发现类定义中有许多Q_PROPERTY的东西,比如最常用的QWidget的类定义: Qt中的Q_PROPERTY宏在Qt中是很常用的,那 ...

  9. 【QT】 QThread部分源码浅析

    本文章挑出QThread源码中部分重点代码来说明QThread启动到结束的过程是怎么调度的.其次因为到了Qt4.4版本,Qt的多线程就有所变化,所以本章会以Qt4.0.1和Qt5.6.2版本的源码来进 ...

随机推荐

  1. 【leetcode❤python】141. Linked List Cycle

    #-*- coding: UTF-8 -*- #Method:快慢指针法,建立虚表头,快指针走两步,慢指针走一步,若存在环,则快指针会追上慢指针# Definition for singly-link ...

  2. Spring MVC无法获取ajax POST的参数和值

    一.怎么会这个样子 很简单的一个想法,ajax以POST的方式提交一个表单,Spring MVC解析.然而一次次的打印null折磨了我整整一天…… 最后的解决现在看来是很明显的问题,“只是当时已惘然” ...

  3. 目前比较全的CSS重设(reset)方法总结

    在当今网页设计/开发实践中,使用CSS来为语义化的(X)HTML标记添加样式风格是 重要的关键.在设计师们的梦想中都存在着这样的一个完美世界:所有的浏览器都能够理解和适用多有CSS规则,并且呈现相同的 ...

  4. vimperator学习教程

    有人总是说Vim难学,而Vimperator把火狐变成了VIM,所以很多人就认为只有那些VIM的狂热者才会用这个插件,但实际上并不是这样.Vimperator在安装之后,只要稍加练习,就可以让我们的浏 ...

  5. 用Spring MVC开发简单的Web应用

    这个例子是来自于Gary Mak等人写的Spring攻略(第二版)第八章Spring @MVC中的一个例子,在此以学习为目的进行记录. 问题:想用Spring MVC开发一个简单的Web应用, 学习这 ...

  6. firefox渗透师必备的利器

    工欲善必先利其器,firefox一直是各位渗透师必备的利器,小编这里推荐34款firefox渗透测试辅助插件,其中包含渗透测试.信息收集.代理.加密解密等功能. 1:Firebug Firefox的 ...

  7. fzu 1015 土地划分(判断线段相交+求出交点+找规律)

    链接:http://acm.fzu.edu.cn/problem.php?pid=1015  Problem 1015 土地划分 Accept: 714    Submit: 1675Time Lim ...

  8. Linux基础01 学会使用命令帮助

    Linux基础01 学会使用命令帮助 概述 在linux终端,面对命令不知道怎么用,或不记得命令的拼写及参数时,我们需要求助于系统的帮助文档:linux系统内置的帮助文档很详细,通常能解决我们的问题, ...

  9. jpg 批量压缩工具 v1.0

    工作需要经常压缩大量图片,网上搜了一些  使用起来总觉得不方便.昨天自己用AIR 写了一个,功能简单,需要的朋友可以自己 下载使用win 版绿色版  http://pan.baidu.com/s/1k ...

  10. C语言单链表实现19个功能完全详解

    谢谢Lee.Kevin分享了这篇文章 最近在复习数据结构,想把数据结构里面涉及的都自己实现一下,完全是用C语言实现的. 自己编写的不是很好,大家可以参考,有错误希望帮忙指正,现在正处于编写阶段,一共将 ...