BUG:由Qt::BlockingQueuedConnection引起的关闭Qt主页面而后台仍有进程残留
1、错误代码示例
首先我们看下下面的代码,可以思考一下代码的错误之处
/** BlockingQueueDeadLock.h **/
#pragma once
#include <QtWidgets/QMainWindow>
#include "ui_BlockingQueueDeadLock.h"
#include <thread>
class BlockingQueueDeadLock : public QMainWindow
{
Q_OBJECT
public:
BlockingQueueDeadLock(QWidget *parent = nullptr);
~BlockingQueueDeadLock();
public slots:
void RecvBlockingQueueSignal();
signals:
void SendBlockingQueueSignal();
private:
void startLoopTest();
void stopLoopTest();
void RunInThread();
private:
Ui::BlockingQueueDeadLockClass ui;
std::thread loopTest;
bool m_StopFlag;
};
/** BlockingQueueDeadLock.cpp **/
#include "BlockingQueueDeadLock.h"
#include <qdebug.h>
BlockingQueueDeadLock::BlockingQueueDeadLock(QWidget *parent)
: QMainWindow(parent)
, m_StopFlag(false)
{
ui.setupUi(this);
startLoopTest();
connect(this, &BlockingQueueDeadLock::SendBlockingQueueSignal,
this, &BlockingQueueDeadLock::RecvBlockingQueueSignal, Qt::BlockingQueuedConnection);
}
BlockingQueueDeadLock::~BlockingQueueDeadLock()
{
stopLoopTest();
}
void BlockingQueueDeadLock::RunInThread()
{
qDebug("%1", std::this_thread::get_id());
while (!m_StopFlag) {
std::this_thread::sleep_for(std::chrono::microseconds(10));
qDebug("signal thread: %1", std::this_thread::get_id());
emit SendBlockingQueueSignal();
}
}
void BlockingQueueDeadLock::RecvBlockingQueueSignal()
{
qDebug("slot thread: %1", std::this_thread::get_id());
}
void BlockingQueueDeadLock::startLoopTest()
{
m_StopFlag = false;
loopTest = std::thread(&BlockingQueueDeadLock::RunInThread, this);
}
void BlockingQueueDeadLock::stopLoopTest()
{
m_StopFlag = true;
if (loopTest.joinable()) {
loopTest.join();
}
}
上面短短几十行代码竟会导致当我关闭Qt主页面时,后台的进程并没有完全退出。
2、原因分析
先使用转储工具获取当前后台进程的堆栈信息;右击后台进程->创建转储文件
这时会获得一个DMP文件,通过windbg分析该DMP文件,如下图所示
我们可以清晰的看到后台进程一直在等待join函数的退出;阅读源码分析join最终调用的时_Thrd_join这个接口,该接口是阻塞的,需要等待线程的主函数运行结束后才会返回。
也就是说我们RunInThread线程主函数迟迟没有结束。
BlockingQueueDeadLock::~BlockingQueueDeadLock()
{
stopLoopTest();
}
void BlockingQueueDeadLock::RunInThread()
{
qDebug("%1", std::this_thread::get_id());
while (!m_StopFlag) {
std::this_thread::sleep_for(std::chrono::microseconds(10));
qDebug("signal thread: %1", std::this_thread::get_id());
emit SendBlockingQueueSignal();
}
}
void BlockingQueueDeadLock::stopLoopTest()
{
m_StopFlag = true;
if (loopTest.joinable()) {
loopTest.join();
}
}
线程的主函数就是一个while循环,在我们BlockingQueueDeadLock析构的时候会将标识符m_StopFlag置为true;按道理讲该while循环应该很快的结束并返回。
但是!但是!但是!我们是否忽略了emit这个信号发射的是如何connect的呢?
connect(this, &BlockingQueueDeadLock::SendBlockingQueueSignal,
this, &BlockingQueueDeadLock::RecvBlockingQueueSignal, Qt::BlockingQueuedConnection);
非常非常奇怪的是为什么connect最后一个参数是Qt::BlockingQueuedConnection呢?我看到这个代码也会很奇怪,可能之前的开发者认为发送信号和接受信号的slot不在同一个线程吧!!
3、解决方案
奇怪的地方必有妖,没错就是Qt::BlockingQueuedConnection这里有问题。
先看Qt官方文档的解释:
非常明确的指出了发射的信号与接收的槽不能在同一个线程里,否者会导致应用死锁。
想要了解Qt BlockingQueuedConnection源码的同学可以看下面的文章:
14.QueuedConnection和BlockingQueuedConnection连接方式源码分析_Master Cui的博客-CSDN博客
明确的指出了使用BlockingQueuedConnection同一个线程时死锁的原因。
因此,我们只需要将Qt::BlockingQueuedConnection最后的参数去除,使用默认参数即可。
这就是我发现的Qt主界面关闭,而后台进程未释放的问题;还是由于后台进程中的某个线程没有释放,而该线程没有结束又是由于Qt::BlockingQueuedConnection死锁导致的。
4、总结
当前Qt主界面关闭,而后台进程未释放的原因有很多。这里只是展示了其中一个原因:后台进程中有线程未及时释放导致的,也为遇到同样问题的你提供一个思路。