Qt做的应用程序无法彻底关闭的问题解析
大家有没有遇到过这样的情况:我们开发的某个应用程序,有时候在点击关闭按钮后,以为程序已经关闭结束,实际上程序进程还在后台运行。
这种有可能是程序中开启了某个子线程,当你关闭的时候,线程还在运行,导致程序看似关闭了,实际由于子线程的运行,并没有停止进程,而是转为在后台运行。
我们模拟一下这种情况。
1.问题描述
应用程序关闭后,去仍然在后台继续运行。
2.复现问题
我们使用QtCreator创建一个桌面应用程序作为测试程序,在“MainWindow”构造函数中启动一个子线程,这个子线程while循环一直运行,用来实时监测下面连接的下位机设备的连接状态。
主程序代码:
//主程序
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);setWindowTitle(QString("测试程序"));//开启线程QThreadPool::globalInstance()->setMaxThreadCount(4);ITask *pTask = new DevConnectTask();if(nullptr == pTask){logger()->info() << __FUNCTION__ << ", create MonitorCurrentTask failed!!!";return ;}QThreadPool::globalInstance()->start(pTask);QObject::connect(pTask, SIGNAL(signal_Result(int, bool)), this, SLOT(slotDevConnect(int, bool)), Qt::QueuedConnection);
}
子线程代码:
//头文件
//这里的父类ITask是继承自QObject和QRunnable的自定义的基类,不需要太纠结
class DevConnectTask : public ITask
{Q_OBJECT
public:DevConnectTask();
protected:void run() override;private:bool m_bToolConnected = false;
signals:void signal_Result(int nDev, bool bStatus);};
//cpp文件
//轮询设备连接状态
DevConnectTask::DevConnectTask()
{qRegisterMetaType<QString >("QString");setAutoDelete(true);
}void DevConnectTask::run()
{logger()->info() << __FUNCTION__ << ", DevConnectTask started---------";bool bTool = false;while (1) {//每隔1秒轮询一次,检测设备的连接状态Command::getInstance()->MonitorDevStatus(bTool, bPower);if(bTool != m_bToolConnected){m_bToolConnected = bTool;//状态改变,发送信号emit signal_Result(Dev, m_bToolConnected);}QThread::msleep(1000);}logger()->info() << __FUNCTION__ << ", DevConnectTask task end------------------";
}
首先,上面的代码逻辑上没有问题,主窗体开启后(或者通过按钮启动)启动子线程来监听外设的连接状况;
其次,程序运行上也没有问题,打开程序后能正常创建子线程,并启动子线程来监听外设的状态改变,也能正常收发状态改变的信号;
最后,关闭程序时发现问题,当你点击左上角关闭按钮后,桌面上貌似程序消失了,但当你打开任务管理器查看时,发现程序实际还在后台一直运行着。
3.分析问题
通过调试我们发现是开启的子线程惹的祸(实际用于正式的工程代码,可能需要调试,日志,及不断地屏蔽代码排除才能确认哪块引起);
那么这个子线程为什么会导致程序的进程并没有在关闭时结束呢?
我们需要回忆一下Qt中QThreadPool的工作原理。QThreadPool管理着一组线程,用于执行QRunnable任务。当调用start()时,任务会被添加到队列中,由线程池中的线程执行。默认情况下,全局线程池会在程序退出时等待所有任务完成,但如果任务中有无限循环或长时间运行的操作(本次测试程序,就是用while循环在无限的工作着),可能会导致等待时间过长,甚至无法退出。
在本测试程序中,当关闭程序时,主界面UI已经被关闭,资源已经释放了,但主进程开启的监听线程,由于while循环并没有因为某个预想的卡断将其退出,还一直在工作着,导致主进程也一直无法销毁,所以在任务管理器看到,程序依然在运行着。
这里在补充一些:程序关闭时线程没有正确停止,导致后台继续运行。其他可能的原因有几个:
- 任务中没有退出条件:如果任务是一个无限循环,没有检查终止条件,线程池会一直等待任务完成,导致程序无法退出。
- 没有正确清理线程池:可能在关闭事件中没有正确请求线程池停止或等待任务完成。
- 任务未设置自动删除:如果QRunnable没有设置setAutoDelete(true),可能导致资源未释放,影响线程池的清理。
4.解决思路
根据上面的问题,我们可能的解决思路有以下几种:
- 确保QRunnable任务可以被中断:在任务中添加一个标志位,定期检查是否需要退出。
- 在程序关闭时触发所有任务的停止标志:在MainWindow的closeEvent中遍历所有任务,设置停止标志。
- 正确清理线程池:使用waitForDone等待任务结束,并设置超时时间,避免无限等待。
- 处理未自动删除的任务:确保QRunnable的自动删除开启,避免内存泄漏。
根据以上思路,得出一下解决方案:
- 我们引入原子变量,通过原子标志位控制任务执行流程,实现可中断的QRunnable任务。
- 在主窗口添加关闭事件处理,在关闭事件closeEvent中协调线程池关闭;
- 线程任务启动与管理这块,我们要加上正确的启动方式:有回应的管理线程任务;
大致流程如下:
5.代码优化
根据上面的解决方案,我们将代码进行优化:
首先,子线程中,我们加入原子标志“m_stopFlag”以及线程结束的信号“finished()”,并且用原子标志“m_stopFlag”控制线程的run()方法里的while循环,当线程结束时,发送信号“finished()”。
子线程代码:
//头文件
//这里的父类ITask是继承自QObject和QRunnable的自定义的基类,不需要太纠结
class DevConnectTask : public ITask
{Q_OBJECT
public:DevConnectTask();
protected:void run() override;private:bool m_bToolConnected = false;
signals:void signal_Result(int nDev, bool bStatus);public:
//设立原子标志std::atomic<bool> m_stopFlag{false};
signals:
//添加结束信号void finished();
};
//cpp文件
//轮询设备连接状态
DevConnectTask::DevConnectTask()
{qRegisterMetaType<QString >("QString");setAutoDelete(true);
}void DevConnectTask::run()
{logger()->info() << __FUNCTION__ << ", DevConnectTask started---------";bool bTool = false;//while循环收到原子标志控制while (!m_stopFlag.load(std::memory_order_acquire)) {//每隔1秒轮询一次,检测设备的连接状态Command::getInstance()->MonitorDevStatus(bTool, bPower);if(bTool != m_bToolConnected){m_bToolConnected = bTool;//状态改变,发送信号emit signal_Result(Dev, m_bToolConnected);}QThread::msleep(1000);}logger()->info() << __FUNCTION__ << ", DevConnectTask task end------------------";//线程结束时发送结束信号emit finished();
}
主程序中,我们添加一个QList的成员变量“m_activeTasks”原来记录子线程(可能有多个子线程,所以用QList),并且加上线程结束信号“finished()”的响应处理,同时,我们要加上关闭事件“closeEvent()”,用以处理当主程序关闭时,如果有线程在循环里运行,就通过原子标志控制其退出循环,结束线程。
主程序代码:
//主程序
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);setWindowTitle(QString("测试程序"));//开启线程QThreadPool::globalInstance()->setMaxThreadCount(4);ITask *pTask = new DevConnectTask();if(nullptr == pTask){logger()->info() << __FUNCTION__ << ", create MonitorCurrentTask failed!!!";return ;}// 加入跟踪列表,m_activeTasks在头文件定义:QList<ITask*> m_activeTasks;m_activeTasks.append(pTask);QObject::connect(pTask, &ITask::finished, [this, pTask](){// 线程结束信号处理m_activeTasks.removeOne(pTask);logger()->info() << __FUNCTION__ << "线程任务完成";});QThreadPool::globalInstance()->start(pTask);QObject::connect(pTask, SIGNAL(signal_Result(int, bool)), this, SLOT(slotDevConnect(int, bool)), Qt::QueuedConnection);
}
//重写关闭事件
void MainWindow::closeEvent(QCloseEvent *event) {// 设置所有任务的停止标志for(auto task : m_activeTasks){task->m_stopFlag.store(true, std::memory_order_release);}// 清理线程池QThreadPool::globalInstance()->clear(); // 移除未开始的任务QThreadPool::globalInstance()->waitForDone(100); // 等待最多100ms// 验证线程池状态logger()->error() << __FUNCTION__ << "关闭前,活动线程数:" << QThreadPool::globalInstance()->activeThreadCount();// 必须调用以完成关闭event->accept();
}
至此,经过验证测试,当关闭程序时,所有线程均能一一结束,确保主进程也彻底结束。大家还有什么其他方法,欢迎留言分享。