头像

张小飞

我有一壶酒,足以慰风尘

《Qt主界面卡死的解决方案-一些具体实现方式》

简介

我们在写UI文件的时候,有很多情况下,是需要界面来处理业务中某些耗时的操作,这时候如果不处理好界面相关的逻辑的话,主界面就会卡死,这时候就需要我们上多线程了

逻辑1

首先上业务上一个很简单的栗子

比如我们的代码中有这么一个耗时的操作

    // 第一种耗时的操作
    auto fWhile1 = [] ()
    {
        for (int i = 0; i < 1000000; i++)
        {
            qDebug()<<i<<endl;
        }
    };

把这个代码绑定到一个按钮事件上

    connect(ui->pushButton1, &QPushButton::clicked, fWhile1);

然后点击。发现界面卡死了,很正常,必须得等到这段代码耗时完成之后才能继续操作界面,这段代码是太不友好了,不清真,所以我们要改一下。


逻辑2

如何改动,可以看下这个函数

QCoreApplication::processEvents

来一起看下官网介绍

Processes all pending events for the calling thread according to the specified flags until there are no more events to process.
You can call this function occasionally when your program is busy performing a long operation (e.g. copying a file).
In the event that you are running a local loop which calls this function continuously, without an event loop, the DeferredDelete events will not be processed. This can affect the behaviour of widgets, e.g. QToolTip, that rely on DeferredDelete events to function properly. An alternative would be to call sendPostedEvents() from within that local loop.
Calling this function processes events only for the calling thread.
Note: This function is thread-safe.

  • You can call this function occasionally when your program is busy performing a long operation (e.g. copying a file).
  • 当程序忙于执行长时间操作(例如复制文件)时,您可以偶尔调用此功能。

我们就暂时就这个(滑稽。
接下来可以把代码搞成这种了。

    auto fWhile2 = [] ()
    {
        for (int i = 0; i < 1000000; i++)
        {
            qDebug()<<i<<endl;
            QApplication::processEvents();
        }
    };
    connect(ui->pushButton2, &QPushButton::clicked, fWhile2);

这种代码在配置不好的机器上实际上还是有点小问题,比如我的小破本子。还是会有点卡的。我觉得用户一般是可以接受这种情况的。


逻辑3

实际上这个逻辑还有一个问题,就是如果我的业务代码不是循环该怎么办呢,这时候我们可以用新的类接口

QtConcurrent::run

这个类。这个类是可以将一个函数放到新的线程里来执行。再加上

QFuture<T>

这个类,可以控制这个新的线程函数开始,控制,结束。
具体可以查看官方文档,我这里就上个简单的栗子

//耗时的操作
static bool function_needmoretime()
{
    for (int i = 0; i < 1000000; i++)
    {
        qDebug()<<i<<endl;
    }
    return true;
}

    // three
    auto fWhile3 = [] () -> void
    {
        QFuture<bool> future = QtConcurrent::run(function_needmoretime);
        while(!future.isFinished())
        {
            QApplication::processEvents(QEventLoop::AllEvents, 100);
        }
    };
    connect(ui->pushButton3, &QPushButton::clicked, fWhile3);

QFuture + QtConcurrent这个框架非常强大,可以将线程同步异步状态抽象出来,让程序员不用太关心这些。这只是一个最简单的栗子。我的小破本子来运行这个是一点都不卡的。界面依旧如丝滑般流畅。


逻辑4-线程

线程基础那种废话我就不多说了。道理大家都懂,我直接上wiki。

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

线程的创建有两种方式,第一种是继承QThread的方式,然后重写run,但是这种方式官方已经不推荐了。官方不推荐的我们就不要这样写了,我们这里讨论的是第二种方式。

继承QObject ,move到新的线程中。

重写 QObject

// 头文件
class workThread : public QObject
{
    Q_OBJECT
public:
    workThread(QObject* parent = nullptr);
    ~workThread();
public slots:
    void start1();
    void doWork();
signals:
    void workFinished();
    void workStart();
};

//cpp
workThread::workThread(QObject* parent) : QObject (parent)
{
}
workThread::~workThread()
{
}
void workThread::start1()
{
    emit workStart();
    doWork();
}
void workThread::doWork()
{
    for (int i = 0; i < 1000000; i++)
    {
        qDebug()<<i<<endl;
    }
    emit workFinished();
}

使用方法

    QThread* m_workerThread = new QThread();
    workThread* worker = new workThread();
    worker->moveToThread(m_workerThread);

    connect(m_workerThread, &QThread::started, worker, &workThread::start1);
    connect(worker, &workThread::workFinished, m_workerThread, &QThread::quit);
    connect(m_workerThread, &QThread::finished, m_workerThread, &QThread::deleteLater);

    //也可以退出释放资源
//    connect(qApp, &QApplication::aboutToQuit, worker, &QObject::deleteLater);
//    connect(worker, &QObject::destroyed, m_workerThread, &QThread::quit);
//    connect(m_workerThread, &QThread::finished, m_workerThread, &QThread::deleteLater);

总结下这样的操作界面是一点都不卡的,因为延迟的操作我们放到新的线程中了。
如果需要传递数据的话,可以将数据通过信号槽的方式传递。

  • 之所以官方不推荐重写QThread也是因为无法使用信号槽
  • 想继承QThread的话也可以,这个继承QThread的类也需要moveToThread,这种做法不清真,所以不希望大家用。

逻辑5 线程 + 定时器

实际上,就是逻辑4的进阶版本,再加个定时器,每隔两秒输出当前时间

class TimerThread : public QObject
{
    Q_OBJECT
public:
    TimerThread(QObject* parent = nullptr);
    ~TimerThread();
public:
    void run();
    void doWork();
signals:
    void workStart();
    void workFinished();
};

static int timerCount = 0;
TimerThread::TimerThread(QObject* parent): QObject (parent)
{
}
TimerThread::~TimerThread()
{
}
void TimerThread::run()
{
    emit workStart();
    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, &TimerThread::doWork);
    timer->start(2000);
}
void TimerThread::doWork()
{
    timerCount ++;
    if (timerCount > 100)
        emit workFinished();
    qDebug()<<QTime::currentTime()<<endl;
}

业务代码在这里

    auto fTimerThreadStart = [=] () -> void
    {
        fiveThread->start();
    };
    connect(ui->threadButton2, &QPushButton::clicked, fTimerThreadStart);
    connect(fiveThread, &QThread::started, timerObject, &TimerThread::run);
    connect(timerObject, &TimerThread::workFinished, fiveThread, &QThread::quit);
    connect(fiveThread, &QThread::finished, fiveThread, &QThread::deleteLater);

界面也是灰常丝滑般流畅的。具体的业务逻辑需求可以再想。

 

 

上一篇:
下一篇:

 评论


 已有6条评论

  1. 熊猫小A 还差那么一点 Windows 10 | 谷歌浏览器 71.0.3578.98 1月前

    继承 QThread 为何无法使用信号槽?应该是可以的啊……只需要在实例化的时候 connect 就行了,我最近就是这么处理的

    • 张小飞 真爱 Ubuntu Linux | 火狐浏览器 61.0 1月前

      熊猫小A你可以试试在qthread中写个connect试试。qthread中绑定的是不响应的。所以很多Qt源码中都会在继承的QThread中强行moveToThread

      • 熊猫小A 还差那么一点 Windows 10 | 谷歌浏览器 71.0.3578.98 1月前

        张小飞噢你是这个意思。我是说实例化时在主线程里用 connect 绑定,是OK的。

        • 张小飞 真爱 Ubuntu Linux | 火狐浏览器 64.0 1月前

          熊猫小A是的,很不方便用,就连官方源码中都用了继承QThread构造函数中movoToThread这种黑魔法

  2. rekols 还差那么一点 Linux | 谷歌浏览器 71.0.3578.98 1月前

    不清真啊 ⌇●﹏●⌇

    • 张小飞 真爱 Ubuntu Linux | 火狐浏览器 64.0 1月前

      rekols你个傻子又换域名了?