Qt5 Windows下QThread执行QProcess后wait()永久卡死问题咨询
Windows平台Qt5实现中存在可复现的线程卡死问题,该问题在Qt6版本中无法复现,核心疑问两点:
- 该现象是Qt5固有bug,还是业务代码编写错误导致
- 项目暂时无法升级Qt6的前提下,有什么可行的规避方案
自行实现了BackgroundExecutor类,内部持有QThread实例,对外提供投递std::function类型任务到后台线程执行的接口;类析构时会调用线程对象的quit()和wait()方法等待线程正常退出。
当后台QThread执行的任务中调用过外部QProcess时(推测该操作会修改线程QEventLoop的状态),调用wait()方法有概率永久卡死。
卡死时调用栈
主线程调用栈
ntdll.dll!NtWaitForSingleObject() Unknown KernelBase.dll!WaitForSingleObjectEx() Unknown > Qt5Cored.dll!QThread::wait(QDeadlineTimer deadline) Line 630 C++ QThreadAndQProcessBug.exe!BackgroundExecutor::~BackgroundExecutor() Line 87 C++ QThreadAndQProcessBug.exe!RunTest() Line 123 C++ QThreadAndQProcessBug.exe!RunTestMultipleTimes() Line 132 C++
后台线程调用栈
win32u.dll!NtUserMsgWaitForMultipleObjectsEx() Unknown user32.dll!RealMsgWaitForMultipleObjectsEx() Unknown > Qt5Cored.dll!QEventDispatcherWin32::processEvents(QFlags<enum QEventLoop::ProcessEventsFlag> flags) Line 625 C++ Qt5Cored.dll!QEventLoop::processEvents(QFlags<enum QEventLoop::ProcessEventsFlag> flags) Line 140 C++ Qt5Cored.dll!QEventLoop::exec(QFlags<enum QEventLoop::ProcessEventsFlag> flags) Line 232 C++ Qt5Cored.dll!QThread::exec() Line 547 C++ Qt5Cored.dll!QThread::run() Line 617 C++ Qt5Cored.dll!QThreadPrivate::start(void * arg) Line 407 C++
卡死位置对应Qt 5.15.2版本qeventdispatcher_win.cpp文件第625行,属于QEventDispatcherWin32::processEvents函数,对应代码行:waitRet = MsgWaitForMultipleObjectsEx(nCount, pHandles, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE | MWMO_INPUTAVAILABLE);
最小复现代码
复现概率受环境影响:部分测试设备平均1000次迭代即可触发卡死,部分设备需要10万次迭代才会出现问题,完整可运行代码如下:
#include <QCoreApplication> #include <QTimer> #include <QObject> #include <QProcess> #include <QThread> #include <functional> #include <future> #include <memory> #include <iostream> Q_DECLARE_METATYPE(std::function<void()>); // 用于通过Qt信号传递std::function<void()>类型 static void EnsureStdFunctionOfVoidMetaTypeRegistered() { static std::once_flag std_function_metatype_registered{}; std::call_once(std_function_metatype_registered, []() { qRegisterMetaType<std::function<void()>>("std::function<void()>"); }); } class WorkerObject; // 运行在BackgroundExecutor后台线程中的工作对象 class BackgroundExecutor final { public: BackgroundExecutor(); ~BackgroundExecutor(); // 向后端线程投递新任务 // 返回std::future可用于等待任务执行完成 [[nodiscard]] std::future<void> PostTask(std::function<void()> task); private: WorkerObject* _background_worker = nullptr; QThread* _qt_thread = nullptr; }; class WorkerObject final : public QObject { Q_OBJECT; public: WorkerObject() { connect(this, &WorkerObject::TaskPosted, this, &WorkerObject::ProcessPostedTask); } // 可从任意线程调用 // 通过Qt信号槽机制将任务传递到后台工作线程执行 void PostTask(const std::function<void()>& task) { EnsureStdFunctionOfVoidMetaTypeRegistered(); Q_EMIT TaskPosted(task); } private Q_SLOTS: void ProcessPostedTask(const std::function<void()>& posted_task) { std::invoke(posted_task); } Q_SIGNALS: void TaskPosted(const std::function<void()>&); }; BackgroundExecutor::BackgroundExecutor() { { std::unique_ptr<QThread> qt_thread_safe(new QThread()); // 异常安全处理 _background_worker = new WorkerObject(); _qt_thread = qt_thread_safe.release(); } _background_worker->moveToThread(_qt_thread); QObject::connect(_qt_thread, &QThread::finished, _background_worker, &WorkerObject::deleteLater); QObject::connect(_qt_thread, &QThread::finished, _qt_thread, &QThread::deleteLater); _qt_thread->start(); } BackgroundExecutor::~BackgroundExecutor() { _qt_thread->quit(); _qt_thread->wait(); // 此处可能卡死 } [[nodiscard]] std::future<void> BackgroundExecutor::PostTask(std::function<void()> task) { std::shared_ptr task_promise = std::make_shared<std::promise<void>>(); std::future task_future = task_promise->get_future(); std::function<void()> task_wrapper = [task_promise = std::move(task_promise), task = std::move(task)]() { std::invoke(task); task_promise->set_value(); }; _background_worker->PostTask(task_wrapper); return task_future; } static void RunQProcessAndWaitForFinished() { QProcess process; process.setProgram("C:\\Windows\\System32\\cmd.exe"); process.setArguments({ "/C", "C:\\Windows\\System32\\timeout.exe", QString::number(30) }); process.start(); process.waitForStarted(-1); process.waitForFinished(-1); } static void RunTest() { BackgroundExecutor executor; std::future task_future = executor.PostTask([]() { RunQProcessAndWaitForFinished(); }); task_future.get(); } static void RunTestMultipleTimes() { constexpr int repeat = 500'000; for (int i = 0; i < repeat; ++i) { std::cout << "starting iteration " << i << '\n'; RunTest(); } std::cout << "all iterations finished" << '\n'; } int main(int argc, char** argv) { QCoreApplication qt_app{ argc, argv }; QTimer::singleShot( 0, [&]() { RunTestMultipleTimes(); qt_app.exit(0); }); return qt_app.exec(); } #include "main.moc"
1. 该问题属于Qt5 Windows平台的固有bug
业务代码本身没有逻辑错误,QThread析构时调用quit()+wait()是Qt官方文档推荐的标准写法。
该问题是Qt5 Win32事件分发器的已知竞态缺陷,Qt6重构了相关事件处理逻辑后问题已修复,因此无法复现。
根因如下:QProcess在Windows平台运行时,会向所在线程的事件分发器注册额外的句柄与窗口消息监听。QProcess析构后存在极低概率的竞态,会残留未清理的消息唤醒标记。此时调用QThread::quit()向线程事件循环投递退出事件时,MsgWaitForMultipleObjectsEx会被残留的无效输入标记误唤醒,但唤醒后事件循环取不到任何有效事件(包括刚投递的退出事件),就会再次进入无限等待状态,最终导致wait()永久卡死。
2. 可行规避方案
按改造成本从低到高排序:
- 方案一:析构前先向后台线程投递一个空任务,等这个空任务执行完成后,再调用
quit()和wait()。该方案利用Qt跨线程事件投递的顺序保证,确保线程退出事件排在所有残留事件之后,实测可以覆盖绝大多数卡死场景,改造成本极低。 - 方案二:自定义Win32事件分发器,重写
processEvents逻辑:当线程收到退出标记时,不再传入INFINITE超时调用MsgWaitForMultipleObjectsEx,将超时时间改为100ms,每次等待超时后主动检查线程退出状态。该方案可以100%解决问题,仅需要在后台线程启动时替换默认事件分发器即可,改造成本中等。 - 方案三:不要在短生命周期的后台
QThread中直接创建QProcess,把所有QProcess的创建、启动、等待逻辑统一放到主线程或者全局常驻的后台线程中执行,业务线程通过任务投递的方式调用进程操作接口,从根源上避免QProcess和短生命周期QThread搭配的场景。
内容的提问来源于stack exchange,提问作者Taras

