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

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

一、多线程操作UI界面的示例

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

首先创建QtGui应用,工程名称为“myThreadBar”,类名选择“QMainWindow”,其他选项保持默认即可。再添加一个名称为WorkerThread的头文件,定义一个WorkerThread类,让其继承自QThread,并重写run()函数,修改workerthread.h文件如下:

#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H #include <QThread>
#include <QDebug> 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);
}; #endif // WORKERTHREAD_H

通过在run()函数中调用msleep(50),线程会每隔50毫秒让当前的进度值加1,然后发射一个resultReady()信号,其余时间什么都不做。在这段空闲时间,线程不占用任何的系统资源。当休眠时间结束,线程就会获得CPU时钟,将继续执行它的指令。

再在mainwindow.ui上添加一个按钮和进度条部件,然后mainwindow.h修改如下:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H #include <QMainWindow>
#include "workerthread.h" namespace Ui {
class MainWindow;
} class MainWindow : public QMainWindow
{
Q_OBJECT public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow(); private slots:
// 更新进度
void handleResults(int value); // 开启线程
void startThread(); private:
Ui::MainWindow *ui; WorkerThread m_workerThread;
}; #endif // MAINWINDOW_H

然后mainwindow.cpp修改如下:

#include "mainwindow.h"
#include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this); qDebug() << "Main Thread : " << QThread::currentThreadId(); // 连接信号槽
this->connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(startThread()));
} MainWindow::~MainWindow()
{
delete ui;
} void MainWindow::handleResults(int value)
{
qDebug() << "Handle Thread : " << QThread::currentThreadId();
ui->progressBar->setValue(value);
} void MainWindow::startThread()
{
WorkerThread *workerThread = new WorkerThread(this);
this->connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
// 线程结束后,自动销毁
this->connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater()));
workerThread->start();
}

由于信号与槽连接类型默认为“Qt::AutoConnection”,在这里相当于“Qt::QueuedConnection”。也就是说,槽函数在接收者的线程(主线程)中执行。

执行程序,“应用程序输出”窗口输出如下:

Main Thread :  0x3140
Worker Thread : 0x3140
Worker Run Thread : 0x2588
Handle Thread : 0x3140

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

二、避免多次connect

当多次点击“开始”按钮的时候,就会多次connect(),从而启动多个线程,同时更新进度条。为了避免这个问题,我们先在mainwindow.h上添加私有成员变量"WorkerThread m_workerThread;",然后修改mainwindow.cpp如下:

#include "mainwindow.h"
#include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this); // 连接信号槽
this->connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(startThread())); this->connect(&m_workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
} MainWindow::~MainWindow()
{
delete ui;
} void MainWindow::handleResults(int value)
{
qDebug() << "Handle Thread : " << QThread::currentThreadId();
ui->progressBar->setValue(value);
} void MainWindow::startThread()
{
if (!m_workerThread.isRunning())
m_workerThread.start();
}

不再在startThread()函数内创建WorkerThread对象指针,而是定义私有成员变量,再将connect添加在构造函数中,保证了信号槽的正常连接。在线程start()之前,可以使用isFinished()和isRunning()来查询线程的状态,判断线程是否正在运行,以确保线程的正常启动。

三、优雅地结束线程的两种方法

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

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

QThread: Destroyed while thread is still running

这是因为次线程还在运行,就结束了UI主线程,导致事件循环结束。这个问题在使用线程的过程中经常遇到,尤其是耗时操作。大多数情况下,当程序退出时,次线程也许会正常退出。这时,虽然抱着侥幸心理,但隐患依然存在,也许在极少数情况下,就会出现Crash。

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

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

方法一

这种方式是Qt4.x中比较常用的,主要是利用“QMutex互斥锁 + bool成员变量”的方式来保证共享数据的安全性。在workerthread.h上继续添加互斥锁、析构函数和stop()函数,修改如下:

#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H #include <QThread>
#include <QMutexLocker>
#include <QDebug> 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;
}; #endif // WORKERTHREAD_H

当主窗口被关闭,其“子对象”WorkerThread也会析构调用stop()函数,使m_bStopped变为true,则break跳出循环结束run()函数,结束进程。当主线程调用stop()更新m_bStopped的时候,run()函数也极有可能正在访问它(这时,他们处于不同的线程),所以存在资源竞争,因此需要加锁,保证共享数据的安全性。

为什么要加锁?

很简单,是为了共享数据段操作的互斥。避免形成资源竞争的情况(多个线程有可能访问同一共享资源的情况)。

方法二

  • Qt5以后,可以使用requestInterruption()、isInterruptionRequested()这两个函数,使用很方便,修改workerthread.h文件如下:
#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H #include <QThread>
#include <QMutexLocker>
#include <QDebug> class WorkerThread : public QThread
{
Q_OBJECT public:
explicit WorkerThread(QObject *parent = nullptr)
: QThread(parent)
{
qDebug() << "Worker Thread : " << QThread::currentThreadId();
} ~WorkerThread()
{
// 请求终止
requestInterruption();
quit();
wait();
} protected:
virtual void run() Q_DECL_OVERRIDE
{
qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
int nValue = 0; // 是否请求终止
while (!isInterruptionRequested())
{
while (nValue < 100)
{
// 休眠50毫秒
msleep(50);
++nValue; // 准备更新
emit resultReady(nValue);
}
} }
signals:
void resultReady(int value);
}; #endif // WORKERTHREAD_H

在耗时操作中使用isInterruptionRequested()来判断是否请求终止线程,如果没有,则一直运行;当希望终止线程的时候,调用requestInterruption()即可。这两个函数内部也使用了互斥锁QMutex。

Qt 之 QThread(深入理解)

Qt 进程和线程之四:线程实际应用的更多相关文章

  1. Qt 进程和线程之二:启动线程

    Qt提供了对线程的支持,这包括一组与平台无关的线程类.一个线程安全的发送事件的方式,以及跨线程的信号槽的关联.这些使得可以很容易地开发可移植的多线程Qt应用程序,可以充分利用多处理器的机器.多线程编程 ...

  2. Qt 学习之路:线程总结

    前面我们已经详细介绍过有关线程的一些值得注意的事项.现在我们开始对线程做一些总结. 有关线程,你可以做的是: 在QThread子类添加信号.这是绝对安全的,并且也是正确的(前面我们已经详细介绍过,发送 ...

  3. Qt 学习之路 :线程简介

    现代的程序中,使用线程的概率应该大于进程.特别是在多核时代,随着 CPU 主频的提升,受制于发热量的限制,CPU 散热问题已经进入瓶颈,另辟蹊径地提高程序运行效率就是使用线程,充分利用多核的优势.有关 ...

  4. PHP CLI编程基础知识积累(进程、子进程、线程)

    .note-content { font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", STHeit ...

  5. java核心知识点学习----并发和并行的区别,进程和线程的区别,如何创建线程和线程的四种状态,什么是线程计时器

    多线程并发就像是内功,框架都像是外功,内功不足,外功也难得精要. 1.进程和线程的区别 一个程序至少有一个进程,一个进程至少有一个线程. 用工厂来比喻就是,一个工厂可以生产不同种类的产品,操作系统就是 ...

  6. Qt 学习之路:线程和 QObject

    前面两个章节我们从事件循环和线程类库两个角度阐述有关线程的问题.本章我们将深入线程间得交互,探讨线程和QObject之间的关系.在某种程度上,这才是多线程编程真正需要注意的问题. 现在我们已经讨论过事 ...

  7. Python 第八篇:异常处理、Socket语法、SocketServer实现多并发、进程和线程、线程锁、GIL、Event、信号量、进程间通讯

    本节内容: 异常处理.Socket语法.SocketServer实现多并发.进程和线程.线程锁.GIL.Event.信号量.进程间通讯.生产者消费者模型.队列Queue.multiprocess实例 ...

  8. Linux线程 之 线程 线程组 进程 轻量级进程(LWP)

    Thread Local Storage,线程本地存储,大神Ulrich Drepper有篇PDF文档是讲TLS的,我曾经努力过三次尝试搞清楚TLS的原理,均没有彻底搞清楚.这一次是第三次,我沉浸gl ...

  9. linux内核——进程,轻量级进程,线程,线程组

    1.进程.轻量级进程.线程.线程组之间的关系 2.及它们的标识相关说明 一.进程.轻量级进程.线程.线程组之间的关系 借助上图说明: 进程P0有四条执行流,即线程, 主线程t0是它的第一个线程,且与进 ...

  10. JAVA线程与线程、进程与进程间通信

    I.线程与线程间通信 一.基本概念以及线程与进程之间的区别联系: 关于进程和线程,首先从定义上理解就有所不同1.进程是什么?是具有一定独立功能的程序.它是系统进行资源分配和调度的一个独立单位,重点在系 ...

随机推荐

  1. appcompat_v7出现红叉解决方法

    右键属性,java build path 勾选Android5.5.2

  2. 分布式版本控制系统—git命令

    一:Git是什么? Git是目前世界上最先进的分布式版本控制系统. 二:SVN与Git的最主要的区别? SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而干活的时候,用的都是自己的电脑,所以 ...

  3. !function(){}()和function(){}()区别

    控制台打印结果如下所示,接下来看一下具体运行,参考https://swordair.com/function-and-exclamation-mark/: 让一个函数声明语句变成了一个表达式

  4. Logcat不显示Application的解决办法

    Window - show view - devices - debug ----2014.12.1------ 只有在DDMS的device中显示进程名,logcat中的Application标签才 ...

  5. VOIP语音编码带宽计算

    VOIP Bandwidth consumption naturally depends on the codec used.  VOIP消耗的带宽一般取决于所使用的语音编码. When calcul ...

  6. python 复制文件流程

    例子代码: [root@master script]# vim copy_file.py #!/usr/bin/python # -*- coding:utf-8 -*- old_file_name ...

  7. python 基础之第十二天(re正则,socket模块)

    In [14]: 'hello-wold.tar.gz'.split('.') Out[14]: ['hello-wold', 'tar', 'gz'] In [15]: import re In [ ...

  8. Swift引用计数器

    ARC概述 和4.2+版本的Xcode对OC的支持一样,Swift也是使用ARC来管理内存,文档是这么描述的: Swift uses Automatic Reference Counting(ARC) ...

  9. Windows Vista for Developers——第四部分:用户帐号控制(User Account Control,UAC)

    作者:Kenny Kerr 翻译:Dflying Chen 原文:http://weblogs.asp.net/kennykerr/archive/2006/09/29/Windows-Vista-f ...

  10. JAVA解析EXCEL(2003和2007)

    本文参考: http://wenku.baidu.com/view/707f07d95022aaea998f0fd1.html http://surfingforrest.iteye.com/blog ...