Qt线程使用(一)直接继承QThread类

前言

在面试Qt相关岗位时,如何使用线程是一个相当高频的问题。而即便已经有了一些开发经验,这个问题依旧可能回答得不出彩,只能算应付过去。

事实上,在Qt中使用线程,主要有四种能够回答的方案,分别是直接继承QThread、moveToThread、Qt::ConCurrent和QThraedPool。

我打算依次将这些内容全部写明白,算是一个总结。

一、QThread

QThread是Qt中的线程类,也是实现线程最为关键核心的类。它的使用较为简单,主要是通过start()、stop()接口来开启和结束线程,然后通过重载run()函数来实现我们想要完成的任务。

说到这里,我想先提及一个概念。那就是什么样的任务需要放到线程中执行?

我的观点是,只要会造成主线程阻塞,哪怕只是短短几百毫秒,都有迁移到线程中执行的意义。那是因为对于客户操作体验来说,流畅性是非常重要的考量。在某些情况下,界面的阻塞允许被接受,但它绝对有优化的空间。比如我们点击按钮后,试图通过网络完成身份登录校验,或者有数据需要写入数据库。此时出现的阻塞,有部分客户勉强接受,有一些客户就会提出优化建议了。

以上是造成短阻塞的任务,通常是希望通过线程来达到异步操作,避免阻塞主线程,进而可能阻塞UI。

然而还有另一种常见情况,长时间的阻塞,或者换句话说,是丢到while循环中不断执行的操作。最常见的就是音视频解码、网络流数据获取这些场景。对于这些任务,放到线程中让它长时间执行显然是必须的决策级方案,但有一些问题也会随之而来。比如线程不会完成任务后自动退出,我们必须小心处理结束线程的流程操作,以及相关资源的析构。再来,就是跨线程的通信问题,while循环通常涉及数据采集,采集到数据后,往往需要发送到外部进行其他的处理,例如图像帧的渲染。在Qt中,通常会使用信号槽来实现这些功能,这有涉及到所谓的第五位参数,ConnectType连接方式。这也是一个面试高频问题,之后我会详细探讨。

二、直接继承QThread

在本节中,我们先实现直接继承QThread的线程使用,这也是最基础和适合新手的使用。

先介绍一下代码结构。我会在默认的MainWindow界面类中,添加成组的开始、结束按钮,用来测试每一组不同的线程使用方式。

然后,我会定义不同的线程类来实现这些功能。

直接放上代码:

WorkerThread类:

cpp 复制代码
#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H

#include <QThread>
#include <atomic>

class WorkerThread : public QThread
{
    Q_OBJECT
public:
    explicit WorkerThread(QObject *parent = nullptr);
    ~WorkerThread() override;

    void thread_start();
    void thread_stop();

protected:
    void run() override;

private:
    std::atomic<bool> m_stopFlag = false;
};

#endif // WORKERTHREAD_H
cpp 复制代码
#include "WorkerThread.h"
#include <QDebug>
#include <QThread>
#include <QDateTime>

WorkerThread::WorkerThread(QObject *parent)
    : QThread(parent)
{
    qDebug() << "WorkerThread created in" << QThread::currentThread();
    m_stopFlag = false;
}

WorkerThread::~WorkerThread()
{
    m_stopFlag = true;

    // 等待线程结束(避免在run()未退出时销毁对象)
    wait();
    qDebug() << "WorkerThread destroyed";
}

void WorkerThread::thread_start()
{
    m_stopFlag = false;
    this->start();
}

void WorkerThread::thread_stop()
{
    qDebug() << "WorkerThread stopped by flag";
    m_stopFlag = true;
}

void WorkerThread::run()
{
    qDebug() << "WorkerThread started in" << QThread::currentThread();

    while (!m_stopFlag.load()) {
        qDebug() << "Worker thread running... (loop)"<<QDateTime::currentDateTime();
        QThread::msleep(500); // 模拟工作(500ms间隔)
    }


    m_stopFlag = true;

    // 这里自动返回QThread的finished信号
    // finished();
}

这个类中,通过重载run函数来实现线程任务,这里主要是通过一个while来定时每半秒打印一次当前时间。

既然涉及到while,就需要处理中断循环的问题,这里直接使用了bool标志m_stopFlag,以及配套的thread_start和thread_stop接口来进行操作。

这里我们避免直接使用QThread的stop()接口来结束线程,不然会有问题。为了确保线程安全等问题,这里的bool类型改用了原子操作std::atomic。其实不用的话,逻辑上也没啥大问题。

当m_stopFlag被置真后,run()会执行结束,此时QThread会自动发送finish信号,无需我们手动发送。

接下来,就是外部的使用和相关信号槽的连接了。

这是界面类MainWindow的部分代码,其实也就两个开始和结束的按钮点击槽函数,以及一个WorkerThread线程类的指针。

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "workerthread.h"
#include "workermanager.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_btn_thread_start_1_clicked();

    void on_btn_thread_stop_1_clicked();

private:
    Ui::MainWindow *ui;

    // 线程1
    WorkerThread * p_thread_1 = nullptr;
};
#endif // MAINWINDOW_H
cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_btn_thread_start_1_clicked()
{
    if(!p_thread_1){
        p_thread_1 = new WorkerThread();
        connect(p_thread_1, &WorkerThread::finished, this, [this](){
            // 理论上就可以安全释放线程类了
            p_thread_1->deleteLater();
            p_thread_1 = nullptr;
        });

        // p_thread_1->start();
        p_thread_1->thread_start();
    }
}

void MainWindow::on_btn_thread_stop_1_clicked()
{
    if(p_thread_1){
        if(p_thread_1->isRunning())
        {
            // 如果线程正在运行,就可以停止
            p_thread_1->thread_stop();
            // 后续的析构放在创建时绑定的信号槽,这样最安全
        }
    }
}

这里采用开始结束时,动态创建和析构资源的方式。

cpp 复制代码
connect(p_thread_1, &WorkerThread::finished, this, [=](){
    // 理论上就可以安全释放线程类了
    p_thread_1->deleteLater();
    p_thread_1 = nullptr;
});

使用时,我们要关注析构资源的时机。当调用了p_thread_1->thread_stop();来停止线程后,线程结束的时机存在一定延时,也就是异步的。这里的延时是不确定的,不要抱有通过定时器延时一秒钟后,直接delete p_thread_1的想法,这可能会导致程序崩溃。

而且,我们应该习惯使用qt的deleteLater来析构类对象,它会自动将析构需求添加到事件队列中,在合适的时机进行释放。并且多次调用deleteLater也是安全的。

至此,就是直接继承QThread来使用线程的方式了。

最后,运行代码,依次点击开始和结束按钮。

安全析构,完美!

三、总结

直接继承QThread的线程使用无疑十分简单和方便,但有一个最为致命的问题,那就是业务代码和线程实现高度耦合。说的直白点,你是将业务代码嵌合在QThread的run函数中,只有实现了线程,才能跑业务代码。

问题来了,如果我希望在主线程中直接跑这段业务代码,我该怎么办呢?这段业务代码完全无法复用,你需要先启动线程,才可以运行这段代码。这面对复杂需求,显然是不合适的设计。

于是将业务代码和线程实现剥离开来,就成了更优的选择。无需将业务代码耦合在run函数内,自然也不需要继承QThread来实现自定义的线程类。

所以,自定义业务类+QThread的搭配使用,就成了qt中主流的使用方式,这也是qt官方推荐的使用方式。

下一节,我将介绍moveToThread的实现。

相关推荐
vortex51 小时前
Bash One-Liners 学习精要指南
开发语言·chrome·bash
Yu_Lijing1 小时前
【个人项目】C++基于websocket的多用户网页五子棋(上)
开发语言·c++·websocket
脏脏a1 小时前
【初阶数据结构】栈与队列:定义、核心操作与代码解析
c语言·开发语言
济宁雪人1 小时前
Java安全基础——序列化/反序列化
java·开发语言
q***01771 小时前
Java进阶--IO流
java·开发语言
lsx2024061 小时前
C语言中的枚举(enum)
开发语言
csbysj20201 小时前
PHP Math
开发语言
小画家~1 小时前
第三十四:golang 原生 pgsql 对应操作
android·开发语言·golang
ulias2121 小时前
初步了解STL和string
开发语言·c++·mfc