背景引言[ GUI主线程 +子线程]

  跟C++11中很像的是,Qt中使用QThread来管理线程,一个QThread对象管理一个线程,在使用上有很多跟C++11中相似的地方,但更多的是Qt中独有的内容。另外,QThread对象也有消息循环exec()函数,即每个线程都有一个消息循环,用来处理自己这个线程的事件。

  QCoreApplication::exec()总是在主线程(执行main()的线程)中被调用,在GUI程序中,主线程也称为GUI线程,是唯一允许执行GUI相关操作的线程,所有要创建的其他线程任务都要依附于主线程。因此,若要创建一个QThread线程任务,前提是必须创建QApplication(or QCoreApplication)对象。

  GUI应用程序开发的时候, 假设应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,多个线程各司其职,不仅可以提高用户体验还可以提升程序的执行效率。

  在 qt 中使用了多线程,有些事项是需要额外注意:

  • 默认的线程在Qt中称之为窗口线程,也叫主线程即GUI线程,负责窗口事件处理或者窗口控件数据的更新
  • 子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理
  • 主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制

一、QThread 线程类 

Qt 中提供了一个QThread 线程类【继承于QObject,区别于QRunnable】,通过这个类就可以创建子线程了。

1.1 常用成员函数

// 构造函数,父类QObject
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;
// 判断子线程是不是在执行任务
bool QThread::isRunning() const; // 获取、设置线程的优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
优先级:
QThread::IdlePriority --> 最低的优先级
QThread::LowestPriority
QThread::LowPriority
QThread::NormalPriority
QThread::HighPriority
QThread::HighestPriority
QThread::TimeCriticalPriority --> 最高的优先级
QThread::InheritPriority --> 子线程和其父线程的优先级相同, 默认是这个
// 退出线程, 停止底层的事件循环
// 退出线程的工作函数
void QThread::exit(int returnCode = 0);
// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是
// 等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);

1.2 信号槽

//等同于exit() 效果,之后也要调 wait() 函数
[slot] void QThread::quit();
// 启动子线程
[slot] void QThread::start(Priority priority = InheritPriority); // 函数用于强制结束线程,不保证数据完整性和资源释放,慎用
[slot] void QThread::terminate(); // 线程中执行完任务后, 发出该信号
[signal] void QThread::finished();
// 开始工作之前发出这个信号, 一般不使用
[signal] void QThread::started();

1.3 静态函数

// 当前执行线程的QThread指针对象
[static] QThread *QThread::currentThread();
// 返回系统上运行的理想线程数 == 和当前电脑的 CPU 核心数相同
[static] int QThread::idealThreadCount();
// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs); // 单位: 毫秒
[static] void QThread::sleep(unsigned long secs); // 单位: 秒
[static] void QThread::usleep(unsigned long usecs); // 单位: 微秒

1.4 任务处理函数

// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();
//线程的起点,在调用start()之后,新创建的线程就会调用run函数,默认实现调用exec(),run函数返回时,线程的执行将结束。

二、QThread的两种方法

2.1 派生QThread类对象的方法(重写Run函数)

2.1.1 使用步骤:

  1. 创建一个继承于QThread线程类的子类MyThread,即派生QThread;
  2. 重写MyThread类中线程任务函数run () 方法,在该函数内编写子线程要处理的具体的业务流程,线程入口;
  3. 在主线程中创建MyThread子线程对象,调用 start () 方法就启动MyThread子线程;

2.1.2 注意事项:

  1. 不能在类的外部调用 run () 方法启动子线程,在外部调用 start () 相当于让 run () 开始运行
  2. 在 Qt 中在子线程中不要操作程序中的窗口类型对象,不允许,如果操作了程序就挂了
  3. 只有主线程才能操作程序中的窗口对象,默认的线程就是主线程,自己创建的就是子线程

2.1.3 例子:

尝试用多线程实现10s耗时的操作:(用按钮触发)

线程类workThread :

//workThread .h
class workThread : public QThread
{
public:
void run();
}; //workThread.cpp
workThread::workThread(QObject* parent)
{}
//线程入口:主要处理的后台业务逻辑或数据更新等
void workThread::run()
{
qDebug() << "当前子线程ID:" << QThread::currentThreadId();
qDebug() << "开始执行线程";
QThread::sleep(10);
qDebug() << "线程结束";
}

窗口主线程中启用子线程:

//Threadtest .h
class Threadtest : public QMainWindow
{
Q_OBJECT public:
Threadtest(QWidget *parent = Q_NULLPTR); private:
Ui::ThreadtestClass ui;
void btn_clicked();
workThread* thread;
}; //threadtest.cpp
Threadtest::Threadtest(QWidget* parent)
: QMainWindow(parent)
{
ui.setupUi(this);
connect(ui.btn_start, &QPushButton::clicked, this, &Threadtest::btn_clicked);
thread = new workThread ; //主线程中创建workThread子线程对象,
} 

void Threadtest::btn_clicked()
{
  qDebug() << "主线程id:" << QThread::currentThreadId();
  thread->start();//启动子线程
}

2.2.moveToThread+槽函数链接绑定线程接口

使用QThread派生类对象的方法创建线程,这种方法存在一个局限性,假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到run()函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护。所以,Qt 提供的第二种线程的创建方式弥补了第一种方式的缺点,用起来更加灵活,就是使用信号与槽的方式,即把在线程中执行的函数(我们可以称之为线程函数)定义为一个槽函数。

2.2.1 使用步骤

  • 创建一个新的类(mywork),让这个类从 QObject 派生,在这个类中添加一个公共的成员函数(working),函数体就是我们要子线程中执行的业务逻辑
  • 在主线程中创建一个 QThread 对象,这就是子线程的对象
  • 在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象)
  • 将 MyWork 对象移动到创建的子线程对象中,需要调用 QObject 类提供的 moveToThread() 方法
  • 启动子线程,调用 start(), 这时候线程启动了,但是移动到线程中的对象并没有工作
  • 调用 MyWork 类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的

2.2.2 代码样例

//MyWork .h
class MyWork : public QObject
{
Q_OBJECT
public:
explicit MyWork(QObject *parent = nullptr);
// 工作函数
void working();
signals:
void curNumber(int num);
public slots:
}; //mywork.cpp
MyWork::MyWork(QObject *parent) : QObject(parent)
{}
void MyWork::working()
{
qDebug() << "当前线程对象的地址: " << QThread::currentThread();
int num = 0;
while(1)
{
emit curNumber(num++);
if(num == 10000000)
break;
QThread::usleep(1);
}
qDebug() << "run() 执行完毕, 子线程退出...";
} //主程序
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "主线程对象的地址: " << QThread::currentThread();
// 创建线程对象
QThread* sub = new QThread ;
// 创建工作的类对象
// 千万不要指定给创建的对象指定父对象
// 如果指定了: QObject::moveToThread: Cannot move objects with a parent
MyWork* work = new MyWork;
// 将工作的类对象移动到创建的子线程对象中
work->moveToThread(sub);
// 启动线程
sub->start();
// 让工作的对象开始工作, 点击开始按钮, 开始工作
connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);//signal对象为主线程,slot函数为子线程,分处不同的线程中
// 显示数据
connect(work, &MyWork::curNumber, this, [=](int num)
{
ui->label->setNum(num);
});
} MainWindow::~MainWindow()
{
delete ui;
}

2.2.3 注意事项

使用这种多线程方式,假设有多个不相关的业务流程需要被处理,那么就可以创建多个类似于 MyWork 的类,将业务流程放多类的公共成员函数中,然后将这个业务类的实例对象移动到对应的子线程中 moveToThread() 就可以了,这样可以让编写的程序更加灵活,可读性更强,更易于维护。使用时注意事项一下:

  • 子线程中不要操作UI:Qt创建的子线程中是不能对UI对象进行任何操作的,即QWidget及其派生类对象。Qt中子线程不能执行任何关于界面的处理,正确的操作应该是通过信号槽,将一些参数传递给主线程,让主线程(也就是Controller)去处理。
  • 对任务类进行声明初始化时,不要指定父对象:比如上面程序中的:MyWork* work = new MyWork;
  • 跨线程的信号槽:QThread与connect的关系中在使用connect函数的时候,我们一般会把最后一个参数忽略掉,即第五个参数。最后一个参数代表的是连接的方式:
     connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);//signal对象为主线程,slot函数为子线程,分处不同的线程中
  1. 自动连接(AutoConnection):默认的连接方式。如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接。
  2. 直接连接(DirectConnection):当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射者所在线程执行。
  3. 队列连接(QueuedConnection):当控制权回到接受者所在线程的事件循环式,槽函数被调用。槽函数在接收者所在线程执行。
  4. 阻塞队列连接(BlockingQueuedConnection):信号和槽必须在不同的线程中,否则就产生死锁,槽函数的调用情形和Queued Connection相同,不同的是当前的线程会阻塞住,直到槽函数返回【emit后阻塞执行,直到slot执行完毕后,才执行emit后续代码】
  5. 唯一连接(UniqueConnection):是配合前四种使用的。确保相同的信号,相同的槽保持唯一连接。作用就是使相同信号唯一连接相同槽。但是你在下一次连接的时候,如果不使用Qt::UniqueConnection,下次连接还是会成功,不会使唯一连接生效。要两次都使用Qt::UniqueConnection,才会生效。

三、 Qt中线程安全问题

QThread继承自QObject,发射信号以指示线程执行开始与结束,并提供了许多槽函数。QObjects可以用于多线程,发射信号以在其它线程中调用槽函数,并且向“存活”于其它线程中的对象发送事件

3.1 线程同步

3.1.1 线程同步基础概念

  • 临界资源:每次只允许一个线程进行访问的资源
  • 线程间互斥:多个线程在同一时刻都需要访问临界资源

线程锁能够保证临界资源的安全性,通常,每个临界资源需要一个线程锁进行保护

线程死锁:线程间相互等待临界资源而造成彼此无法继续执行。

产生死锁的条件:

  • 系统中存在多个临界资源且临界资源不可抢占
  • 线程需要多个临界资源才能继续执行

QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了线程同步的手段。使用线程的主要想法是希望它们可以尽可能并发执行,而一些关键点上线程之间需要停止或等待

3.1.2 互斥锁QMutex、QMutexLocker、QWaitCondition

  • QMutex 提供相互排斥的锁,或互斥量。在一个时刻至多一个线程拥有mutex,假如一个线程试图访问已经被锁定的mutex,那么线程将休眠,直到拥有mutex的线程对此mutex解锁,QMutex常用来保护共享数据访问,如果使用了Mutex.lock()而没有对应的使用Mutex.unlcok()的话就会造成死锁,其他的线程将永远也得不到接触Mutex锁住的共享资源的机会;QMutexLocker类似于c++中std::mutex
  • 在较复杂的函数和异常处理中对QMutex类mutex对象进行lock()和unlock()操作将会很复杂,而且开销也大,所以Qt引进了QMutex的辅助类QMutexLocker来避免lock()和unlock()操作。在函数需要的地方建立QMutexLocker对象,并把mutex指针传给QMutexLocker对象,此时mutex已经加锁,等到退出函数后,QMutexLocker对象局部变量会自己销毁,此时mutex解锁。QMutexLocker类似于c++中std::lock_guard<>
  • QWaitCondition 允许线程在某些情况发生时唤醒另外的线程。一个或多个线程可以阻塞等待QWaitCondition , 用wakeOne()或wakeAll()设置一个条件。wakeOne()随机唤醒一个,wakeAll()唤醒所有。QWaitCondition 类似于c++中condition_variable

3.2 QObject的可重入性问题【QTcpsocket中使用多线程技术

一个线程安全的函数可以同时被多个线程调用,甚至调用者会使用共享数据也没有问题,因为对共享数据的访问是串行的。一个可重入函数也可以同时被多个线程调用,但是每个调用者只能使用自己的数据。因此,一个线程安全的函数总是可重入的,但一个可重入的函数并不一定是线程安全的。

一个可重入的类,指的是类的成员函数可以被多个线程安全地调用,只要每个线程使用类的不同的对象。而一个线程安全的类,指的是类的成员函数能够被多线程安全地调用,即使所有的线程都使用类的同一个实例。

QObject是可重入的,QObject的大多数非GUI子类如 QTimer、QTcpSocket、QUdpSocket、QHttp、QFtp、QProcess也是可重入的,在多个线程中同时使用这些类是可能的。可重入的类被设计成在一个单线程中创建与使用,在一个线程中创建一个对象而在另一个线程中调用该对象的函数,不保证能行得通。有三种约束需要注意:

  1. QObject实例必须被创建在它父类所被创建的线程中。这意味着,一般情况下永远不要把QThread对象(this)作为该线程中创建的一个对象的父亲(因为QThread对象自身被创建在另外一个线程中,即 QThread* t =new QThread,不要(this))。
  2. 事件驱动的对象可能只能被用在一个单线程中。特别适用于计时器机制(timer mechanism)和网络模块。例如:不能在不属于这个对象的线程中启动一个定时器或连接一个socket,必须保证在删除QThread之前删除所有创建在这个线程中的对象(thread中创建的对象需要在线程释放前释放改对象)。在run()函数的实现中,通过在栈中创建这些对象,可以轻松地做到这一点。
  3. 虽然QObject是可重入的,但GUI类,尤其是QWidget及其所有子类都不是可重入的,只能被用在GUI线程中。QCoreApplication::exec()必须也从GUI线程被调用

在实践中,只能在主线程而非其它线程中使用GUI的类,可以很轻易地被解决:将耗时操作放在一个单独的工作线程中,当工作线程结束后在GUI线程中由屏幕显示结果。一般来说,在QApplication前创建QObject是不行的,会导致奇怪的崩溃或退出,取决于平台。因此,不支持QObject的静态实例。一个单线程或多线程的应用程序应该先创建QApplication,并最后销毁QObject

3.3 线程的事件循环

  • 每个线程都有自己的事件循环。主线程通过QCoreApplication::exec()来启动自己的事件循环, 但对话框的GUI应用程序,有些时候用QDialog::exec(),其它线程可以用QThread::exec()来启动事件循环。就像 QCoreApplication,QThread提供一个exit(int)函数和quit()槽函数,这里要注意与wait()搭配使用。
  • 信号槽机制让发射(发射线程)连接到接收线程中:的事件循环使得线程可以利用一些非GUI的、要求有事件循环存在的Qt类(例如:QTimer、QTcpSocket、和QProcess),使得连接一些线程的信号到一个特定线程的槽函数成为可能。
  • 一个QObject实例被称为存活于它所被创建的线程中。关于这个对象的事件被分发到该线程的事件循环中。可以用QObject::thread()方法获取一个QObject所处的线程。QObject::moveToThread()函数改变一个对象和及其子对象的线程所属性。(如果对象有父对象的话,对象不能被移动到其它线程中)。
  • 从另一个线程(不是QObject对象所属的线程)对该QObject对象调用delete方法是不安全的,除非能保证该对象在那个时刻不处理事件,使用QObejct::deleteLater()更好。一个DeferredDelete类型的事件将被提交(posted),而该对象的线程的 件循环最终会处理这个事件。默认情况下,拥有一个QObject的线程就是创建QObject的线程,而不是 QObject::moveToThread()被调用后的。
  • 如果没有事件循环运行,事件将不会传递给对象。例如:在一个线程中创建了一个QTimer对象,但从没有调用exec(),那么QTimer就永远不会发射timeout()信号,即使调用deleteLater()也不行。(这些限制也同样适用于主线程)。
  • 利用线程安全的方法QCoreApplication::postEvent(),可以在任何时刻给任何线程中的任何对象发送事件,事件将自动被分发到该对象所被创建的线程事件循环中。
  • 所有的线程都支持事件过滤器,而限制是监控对象必须和被监控对象存在于相同的线程中。QCoreApplication::sendEvent()(不同于postEvent())只能将事件分发到和该函数调用者相同的线程中的对象。

工程实践中,为了避免冻结主线程的事件循环(即避免因此而冻结了应用的UI),所有的计算工作是在一个单独的工作线程中完成的,工作线程结束时发射一个信号,通过信号的参数将工作线程的状态发送到GUI线程的槽函数中更新GUI组件状态

Qt多线程编程之QThread的更多相关文章

  1. QT核心编程之Qt线程 (c)

    QT核心编程之Qt线程是本节要介绍的内容,QT核心编程我们要分几个部分来介绍,想参考更多内容,请看末尾的编辑推荐进行详细阅读,先来看本篇内容. Qt对线程提供了支持,它引入了一些基本与平台无关的线程类 ...

  2. iOS多线程编程之NSThread的使用

      目录(?)[-] 简介 iOS有三种多线程编程的技术分别是 三种方式的有缺点介绍 NSThread的使用 NSThread 有两种直接创建方式 参数的意义 PS不显式创建线程的方法 下载图片的例子 ...

  3. iOS多线程编程之NSThread的使用(转)

    本文由http://blog.csdn.net/totogo2010/原创 1.简介: 1.1 iOS有三种多线程编程的技术,分别是: 1..NSThread 2.Cocoa NSOperation  ...

  4. [转] iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用

    介绍: Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其他的对称多处理系统的系统.这建立在任务并行执行的线程池模式的基础上的.它首 ...

  5. [转] iOS多线程编程之NSOperation和NSOperationQueue的使用

    <iOS多线程编程之NSThread的使用> 介绍三种多线程编程和NSThread的使用,这篇介绍NSOperation的使用. 使用 NSOperation的方式有两种, 一种是用定义好 ...

  6. [转]iOS多线程编程之NSThread的使用

    1.简介: 1.1 iOS有三种多线程编程的技术,分别是: 1..NSThread 2.Cocoa NSOperation (iOS多线程编程之NSOperation和NSOperationQueue ...

  7. iOS 多线程编程之Grand Central Dispatch(GCD)

    介绍: Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其它的对称多处理系统的系统.这建立在任务并行运行的线程池模式的基础上的. 它 ...

  8. 深入浅出Cocoa多线程编程之 block 与 dispatch quene

    深入浅出 Cocoa 多线程编程之 block 与 dispatch quene 罗朝辉(http://www.cppblog.com/kesalin CC 许可,转载请注明出处 block 是 Ap ...

  9. iOS多线程编程之NSThread的使用(转载)

    1.简介: 1.1 iOS有三种多线程编程的技术,分别是: 1.NSThread 2.Cocoa NSOperation (iOS多线程编程之NSOperation和NSOperationQueue的 ...

  10. iOS多线程编程之NSOperation和NSOperationQueue的使用

    前一篇 <iOS多线程编程之NSThread的使用> 介绍三种多线程编程和NSThread的使用,这篇介绍NSOperation的使用. 使用 NSOperation的方式有两种, 一种是 ...

随机推荐

  1. LG P3653 小清新数学题

    \(\text{Poblem}\) 求 \(\sum_{i=l}^r \mu(i)\) \(1 \le l,r \le 10^{18}, r - l \le 10^5\) \(\text{Analys ...

  2. Ubuntu18完全卸载php7.2

    转载csdn: Ubuntu18完全卸载php7.2_yisonphper的博客-CSDN博客_ubuntu 卸载php8

  3. Oracle存储过程的创建实例和调用实例

    --编写一个存储过程,给emp表中添加数据.  --方法一:create or replace procedure add_employee(       eno number,       name ...

  4. 【linux系统安装】Anolis OS-龙蜥操作系统实机安装流程整理

    [安装准备] 1.准备一个U盘,可储存空间不低于20G,U盘内资料移出去,待会儿要格式化做U盘启动盘 2.windows操作系统上下载"Rufus",官网:http://rufus ...

  5. Day 24 24.2:逆向分析2 - 完美世界案例

    完美世界逆向分析 url:https://passport.wanmei.com/login?location=L3NhZmUv 定位到正确的断点位置 进行js改写操作 断点代码的关键字:setPub ...

  6. RestTemplate 请求

    @Autowired private RestTemplate httpRestTemplate; String code= request.getParameter("code" ...

  7. 重置Beyond Compare 4试用时间

    重置Beyond Compare 4试用时间 什么是Beyound Compare Beyond Compare是一套由Scooter Software推出的文件比较工具.主要用途是对比两个文件夹或者 ...

  8. 解决MySQL5.5MySQLInstanceConfig最后一步setting报错

    问题描述 在安装过MySQL(或已卸载)的电脑中重新安装MySQL5.5, 在安装最后一项中Processing configuration中最后一项配置失败: 问题解决: 首先关于卸载: 安装时候若 ...

  9. switch和if

    #include<stdio.h> int main() { char ch1='A'; char ch2='B'; switch(ch1) { case'A': switch(ch2) ...

  10. Vue 实现点击空白处隐藏某节点

    手动监听判断 <template> <div> <span ref="projectButton"> <el-popover v-mode ...