簡體   English   中英

Qt 5 - 如何讓 QThread 退出其“等待” function 在 Windows 上執行 QProcess 后卡住?

[英]Qt 5 - How to make QThread exit from its `wait` function where it got stuck after executing a QProcess on Windows?

我相信我在 Windows 的 Qt 5 實現中發現了一個錯誤。 它不能用 Qt 6 重現,所以我認為我現在不應該將它發布給 Qt 的維護人員。 但是我仍然想在這里問(1)它是否確實是一個錯誤(或者我的代碼在某處不正確),以及(2)我可以寫什么解決方法來避免這個問題,前提是我不能升級到 Qt 6現在。

我有一個 class BackgroundExecutor ,它擁有一個QThread並有一個 function 用於向其發布新任務( std::function實例)。 在其析構函數中, BackgroundExecutor調用其線程 object 的quitwait成員函數。

當后台QThread處理的已發布任務之一恰好執行了一些外部QProcess時,事情變得有趣了(我認為它會以某種方式影響線程 QEventLoop 的QEventLoop )。 在這種情況下, QThread上的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++

It's stuck at the line 625 (as of Qt 5.15.2) of "qeventdispatcher_win.cpp" , inside the QEventDispatcherWin32::processEvents function: waitRet = MsgWaitForMultipleObjectsEx(nCount, pHandles, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE | MWMO_INPUTAVAILABLE); .

重現問題的程序的全文(盡管可能需要一些時間 - 我的一台 PC 平均只需要 1000 次迭代,而另一台可能會在掛起之前執行 100'000 次迭代):

#include <QCoreApplication>
#include <QTimer>
#include <QObject>
#include <QProcess>
#include <QThread>

#include <functional>
#include <future>
#include <memory>
#include <iostream>

Q_DECLARE_METATYPE(std::function<void()>); // for passing std::function<void()> through Qt's signals

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; // worker object that "lives" in a background thread of a BackgroundExecutor

class BackgroundExecutor final
{
public:
  BackgroundExecutor();
  ~BackgroundExecutor();

  // posts a new task for the background QThread,
  // returns a std::future which can be waited on to ensure the task is done
  [[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);
  }

  // can be called from any thread;
  // "moves" the task to the background worker thread via Qt's signals/slots mechanism
  // so that it could be processed there
  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()); // exception safety
    _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(); // !!! might hang !!!
}

[[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"

在 Qt 5.12.12 中,所有迭代都已完成。 你試過這個版本嗎?

我只是將類拆分為單獨的文件以隱式包含 moc 文件,然后制作了一個 .pro 文件。

QT += core

CONFIG += c++17 console
CONFIG -= app_bundle

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    backgroundexecutor.cpp \
    main.cpp \
    workerobject.cpp

HEADERS += \
    backgroundexecutor.h \
    workerobject.h

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

發布此問題后,我還按照評論中的建議對 Qt 5.12.12 和 Qt 5.15.5 進行了一些測試。 我可以在這兩個版本中都沒有重現該錯誤,而在 Qt 5.15.2 中它繼續持續重現。 這可能意味着這些版本中確實不存在該錯誤,或​​者那里很少重現(至少在我的 PC 上)並且我嘗試得不夠多。

無論如何,我想分享一個解決方法,該解決方法是在咨詢了一位在 WinAPI 方面比我有更多經驗的同事之后提出的。 如果他們碰巧遇到存在此錯誤的 Qt 版本,希望它也能對其他人有所幫助。

這是代碼(您可以使用 WinMerge 等工具將其與問題中的原始列表進行比較,或者只查看WORKAROUND_FOR_QTHREAD_WAIT宏附近的所有部分):

#include <QCoreApplication>
#include <QTimer>
#include <QObject>
#include <QProcess>
#include <QThread>

#include <atomic>
#include <thread>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <cassert>
#include <iostream>

#ifdef _WIN32
#define WORKAROUND_FOR_QTHREAD_WAIT 1

#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#endif // _WIN32

Q_DECLARE_METATYPE(std::function<void()>); // for passing std::function<void()> through Qt's signals

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; // worker object that "lives" in a background thread of a BackgroundExecutor

class BackgroundExecutor final
{
public:
  BackgroundExecutor();
  ~BackgroundExecutor();

  // posts a new task for the background QThread,
  // returns a std::future which can be waited on to ensure the task is done
  [[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);
  }

  // can be called from any thread;
  // "moves" the task to the background worker thread via Qt's signals/slots mechanism
  // so that it could be processed there
  void PostTask(const std::function<void()>& task)
  {
    EnsureStdFunctionOfVoidMetaTypeRegistered();
    Q_EMIT TaskPosted(task);
  }

#if WORKAROUND_FOR_QTHREAD_WAIT
  [[nodiscard]] DWORD GetBackgroundThreadID() const
  {
    while (_windows_thread_id_initialized.load(std::memory_order_acquire) == false)
      std::this_thread::yield();
    return _windows_thread_id;
  }
#endif // WORKAROUND_FOR_QTHREAD_WAIT

public Q_SLOTS:
  void InitializeBackgroundThreadID()
  {
#if WORKAROUND_FOR_QTHREAD_WAIT
    _windows_thread_id = ::GetCurrentThreadId();
    _windows_thread_id_initialized.store(true, std::memory_order_release);
#else
    assert(false && "shouldn't have been invoked in this configuration");
#endif // WORKAROUND_FOR_QTHREAD_WAIT
  }

  void ProcessPostedTask(const std::function<void()>& posted_task)
  {
    std::invoke(posted_task);
  }

Q_SIGNALS:
  void TaskPosted(const std::function<void()>&);

#if WORKAROUND_FOR_QTHREAD_WAIT
private:
  DWORD _windows_thread_id;
  std::atomic<bool> _windows_thread_id_initialized{ false };
#endif // WORKAROUND_FOR_QTHREAD_WAIT
};

BackgroundExecutor::BackgroundExecutor()
{
  {
    std::unique_ptr<QThread> qt_thread_safe(new QThread()); // exception safety
    _background_worker = new WorkerObject();
    _qt_thread = qt_thread_safe.release();
  }

  _background_worker->moveToThread(_qt_thread);

#if WORKAROUND_FOR_QTHREAD_WAIT
  QMetaObject::invokeMethod(_background_worker, "InitializeBackgroundThreadID", Qt::QueuedConnection);
#endif // WORKAROUND_FOR_QTHREAD_WAIT

  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();
#if WORKAROUND_FOR_QTHREAD_WAIT
  const DWORD background_thread_id = _background_worker->GetBackgroundThreadID();
  while (_qt_thread->wait(1'000) == false)
    // "awaken" the MsgWaitForMultipleObjectsEx function call in which the background thread got stuck
    // by posting a fake "message" to it, so that it would snap out of it, check its exit flag and finish properly
    ::PostThreadMessage(background_thread_id, WM_NULL, 0, 0);
#else
  _qt_thread->wait();
#endif // WORKAROUND_FOR_QTHREAD_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"


現在是實現細節。

基本思想是: MsgWaitForMultipleObjectsEx(..., QS_ALLINPUT,...) function 調用(其中后台線程卡住)可以被發布到該線程隊列的消息“喚醒”,因為QS_ALLINPUT還暗示QS_POSTMESSAGE作為它的“喚醒面具”的一部分。 這可以通過從銷毀(主)線程調用PostThreadMessage來完成,以防我們檢測到后台線程卡住了。 而且由於我們不關心特定類型的消息(實際上也不想發送任何“有意義的”消息),所以WM_NULL可以解決問題。

因此,我們的BackgroundExecutor class 的析構函數應該如下所示:

BackgroundExecutor::~BackgroundExecutor()
{
  _qt_thread->quit();

  const DWORD background_thread_id = _background_worker->GetBackgroundThreadID();
  while (_qt_thread->wait(1'000) == false)
    ::PostThreadMessage(background_thread_id, WM_NULL, 0, 0);
}

現在的問題是,我們如何獲得這個DWORD background_thread_id值。 Qt doesn't provide us an easy way of getting it from a QThread object (there is a function currentThreadId , but it is static and returns a DWORD ID of the currently executing thread, thus it's not what we want here). 相反,我們將在后台線程執行的早期調用GetCurrentThreadId ,將其存儲在我們的WorkerObject中,稍后在主線程中檢索。

為了做到這一點,我們可以在WorkerObject class 中編寫一個新的槽InitializeBackgroundThreadID ,並在將工作線程 ZA8CFDE6331BD59EB2AC96F8911C4 從BackgroundExecutor的構造函數中通過Qt::QueuedConnection調用它。該線程):

public Q_SLOTS:
  void InitializeBackgroundThreadID()
  {
    _windows_thread_id = ::GetCurrentThreadId();
    _windows_thread_id_initialized.store(true, std::memory_order_release);
  }

(這里, _windows_thread_id_windows_thread_id_initializedDWORD成員變量,而WorkerObject是通過synchronizes-with關系進行跨線程同步所需的std::atomic<bool>保護標志)

BackgroundExecutor的構造函數中:

  ...
  _background_worker->moveToThread(_qt_thread);
  QMetaObject::invokeMethod(_background_worker, "InitializeBackgroundThreadID", Qt::QueuedConnection);
  ...

現在我們可以為這個線程 ID 實現 getter:

  [[nodiscard]] DWORD GetBackgroundThreadID() const
  {
    while (_windows_thread_id_initialized.load(std::memory_order_acquire) == false)
      std::this_thread::yield();
    return _windows_thread_id;
  }

實際上,在調用BackgroundExecutor的析構函數時,線程 ID 已經初始化,因此循環根本不會旋轉或調用yield :只需要保證synchronizes-with關系(主要線程將保證在通過加載獲取操作從_windows_thread_id_initialized讀取true讀取_windows_thread_id的正確值)。

可以有其他方法來實現這部分,例如,我們可以使DWORD _windows_thread_id成員變量本身成為atomic ,為“未初始化” state 提供一些“無效”哨兵值(Raymond Chen曾經寫過零應該可以) . 但是這些只是實現細節,基本思想還是一樣的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM