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的实现。

相关推荐
初恋叫萱萱7 小时前
构建高性能生成式AI应用:基于Rust Axum与蓝耘DeepSeek-V3.2大模型服务的全栈开发实战
开发语言·人工智能·rust
cyforkk8 小时前
12、Java 基础硬核复习:集合框架(数据容器)的核心逻辑与面试考点
java·开发语言·面试
我材不敲代码12 小时前
Python实现打包贪吃蛇游戏
开发语言·python·游戏
身如柳絮随风扬13 小时前
Java中的CAS机制详解
java·开发语言
-dzk-14 小时前
【代码随想录】LC 59.螺旋矩阵 II
c++·线性代数·算法·矩阵·模拟
韩立学长14 小时前
【开题答辩实录分享】以《基于Python的大学超市仓储信息管理系统的设计与实现》为例进行选题答辩实录分享
开发语言·python
froginwe1114 小时前
Scala 循环
开发语言
m0_7066532315 小时前
C++编译期数组操作
开发语言·c++·算法
故事和你9115 小时前
sdut-Java面向对象-06 继承和多态、抽象类和接口(函数题:10-18题)
java·开发语言·算法·面向对象·基础语法·继承和多态·抽象类和接口
Bruk.Liu15 小时前
(LangChain实战2):LangChain消息(message)的使用
开发语言·langchain