[英]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 的quit
和wait
成員函數。
當后台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_initialized
的DWORD
成員變量,而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.