You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 20:36:20