[英]QThread: Destroyed while thread is still running in QTest
我寫了一個小的多線程消息隊列。 現在我正在嘗試做一個測試。 但是我遇到了無法解決的同步問題。
如果我刪除有關隊列的所有內容,最小可驗證示例如下所示(抱歉,它有點大):
#include <QCoreApplication>
#include <QDebug>
#include <QTimer>
#include <QtTest/QtTest>
#include <random>
#include <thread>
static std::vector< int > dummy;
class Writer : public QObject {
Q_OBJECT
public:
Writer( QObject *parent = nullptr ) : QObject( parent ) { }
Writer( unsigned, QObject *parent = nullptr ) : QObject( parent ) { }
public:
const std::vector< int > &beginPendings( ) { return dummy; }
void endPendings( ) { }
Q_SIGNALS:
void haveEvents( );
public Q_SLOTS:
void start( ) {
m_stop.test_and_set( );
connect( &m_inputTimer, &QTimer::timeout, this, &Writer::onTimer );
m_inputTimer.start( 100U );
connect( &m_outputTimer, &QTimer::timeout, this, &Writer::notifyEvents );
m_outputTimer.start( 250U );
}
void stop( ) {
m_inputTimer.stop( );
m_outputTimer.stop( );
}
private Q_SLOTS:
void onTimer( ) {
int limit = dist( mt );
for( int idx = 0; idx < limit; ++idx ) {
++m_idx;
}
}
void notifyEvents( ) {
emit haveEvents( );
}
private:
QTimer m_inputTimer;
QTimer m_outputTimer;
int m_idx = 0;
std::atomic_flag m_stop = ATOMIC_FLAG_INIT;
std::random_device rd;
std::mt19937 mt{ rd( ) };
std::uniform_int_distribution< int > dist{ 1, 20 };
};
class InOutQueueTest: public QObject {
Q_OBJECT
public Q_SLOTS:
void onPendingEvents( void ) {
writer->endPendings( );
}
void onTimeout( ) {
writer->stop( );
backendThread->exit( 0 );
backendThread->deleteLater( );
stop = true;
}
private Q_SLOTS:
void limit15( ) {
finishTimer.setSingleShot( true );
finishTimer.start( 5000 );
backendThread = new QThread( );
writer = new Writer( 15U );
connect( &finishTimer, &QTimer::timeout, this, &InOutQueueTest::onTimeout );
connect( writer, &Writer::haveEvents, this, &InOutQueueTest::onPendingEvents );
writer->moveToThread( backendThread );
backendThread->start( );
writer->start( );
while( !stop ) {
QCoreApplication::processEvents( );
}
}
private:
Writer *writer;
QThread *backendThread;
int last = 0;
QTimer finishTimer;
bool stop = false;
};
QTEST_GUILESS_MAIN( InOutQueueTest )
#include "inoutqueue.moc"
我希望測試持續 5 秒,並正確結束。 但是,我得到:
WARNING: InOutQueueTest::limit15() 生成 19 個數字,從 517 開始
位置:[/home/juanjo/Trabajos/qml/test/inoutqueue.cpp(53)]
WARNING: InOutQueueTest::limit15() 生成 19 個數字,從 536 開始
位置:[/home/juanjo/Trabajos/qml/test/inoutqueue.cpp(53)]
QFATAL: InOutQueueTest::limit15() QThread: Destroyed while thread is still running
FAIL: : InOutQueueTest:.limit15() 收到致命錯誤。
位置:[未知文件(0)]
總計:1 次通過,1 次失敗,0 次跳過,0 次列入黑名單,5068 毫秒
********* InOutQueueTest 測試完畢*********
中止
應該結束測試的代碼(5秒后)是這樣的:
void onTimeout( ) {
writer->stop( );
backendThread->exit( 0 );
backendThread->deleteLater( );
stop = true;
}
我在Writer
中調用stop( )
方法,在輔助線程中調用exit( )
方法,並在事件循環的下一次迭代中將其刪除(這是理論)。
同樣,在Writer
class 中, stop( )
方法是:
void stop( ) {
m_inputTimer.stop( );
m_outputTimer.stop( );
}
我只是停止兩個計時器。
我究竟做錯了什么?
我該如何解決?
我寫了一個小的多線程消息隊列
為什么? Qt 本來就已經有一個......將QEvent
s 發布到QObject
s - 它以線程安全的方式完成,支持事件優先級,在刪除接收器時自動排空等。甚至還支持QEvent
的自定義分配器,所以如果您願意,可以使用專用的 memory 池。 當然,專用隊列可能更快,但這需要基准測試和一些 Qt 自己的隊列性能不足的要求。 多線程消息隊列具有令人討厭的極端情況,因為接收者遍歷線程等 - Qt 的事件隊列代碼不那么簡單是有原因的。 想象最終你會重新實現所有這些並不牽強,所以最好有一個很好的理由:)
首先,讓我們注意包含可以更簡單 - 並且永遠不需要QtModule/QtInclude
格式 - 只有在應用程序構建配置錯誤時才有幫助。 不這樣做可以讓您在構建的早期知道問題 - 否則它會在鏈接時失敗。 因此:
#include <QtCore>
#include <QtTest>
只發出信號的插槽是不必要的:信號是可調用的,您可以將任何可調用的東西直接連接到信號 - 在 Qt 4 語法中。 在現代 Qt 語法中,插槽並不是真正需要的,除非您需要它們用於元數據(例如, QtTest
使用它們來檢測測試實現)。 否則,將信號連接到任何方法,即使在非 QObjects 中,或連接到仿函數(例如 lambda)。
現在嚴重的問題:
定時器只能從它們所在的線程中操作。
將計時器移動到其用戶 object 所在的同一線程。這最好通過讓父 object擁有計時器來完成 - 然后移動會自動發生。 請注意, QObject
所有權與按值保留計時器絕不會發生沖突——不會有雙重刪除,因為當父QObject
開始刪除子項時,計時器早已不復存在。
斷言操作定時器的方法是從正確的線程調用的。
可選地提供方便以允許跨線程屏障傳播調用。
每次啟動編寫器時,您都會將計時器重新連接到它們的插槽。 那永遠是不正確的。 此類連接應根據所涉及對象的 scope 進行,確保僅發生一次。 在這種情況下,由於計時器與封閉的 object 一樣長,因此連接屬於構造函數。
#include <random>
#include <vector>
class Writer : public QObject {
Q_OBJECT
static constexpr bool allowCrossThreadConvenience = false;
bool invokedAcrossThreads(auto method) {
bool crossThreadCall = QThread::currentThread() == thread();
if (crossThreadCall && allowCrossThreadConvenience) {
QMetaObject::invokeMethod(this, method);
return true;
}
Q_ASSERT(!crossThreadCall);
return false;
}
public:
Writer(QObject *parent = nullptr) : QObject(parent) {
connect(&m_inputTimer, &QTimer::timeout, this, &Writer::onTimer);
connect(&m_outputTimer, &QTimer::timeout, this, &Writer::haveEvents);
}
const auto &beginPendings() {
static std::vector<int> dummy;
return dummy;
}
void endPendings() { }
Q_SIGNAL void haveEvents();
Q_SLOT void start() {
if (invokedAcrossThreads(&Writer::start)) return;
m_outputTimer.start(250);
m_inputTimer.start(100);
}
Q_SLOT void stop() {
if (invokedAcrossThreads(&Writer::stop)) return;
m_inputTimer.stop();
m_outputTimer.stop();
}
private:
void onTimer() {
m_idx += dist(mt);
}
QTimer m_inputTimer{this};
QTimer m_outputTimer{this};
int m_idx = 0;
std::mt19937 mt{std::random_device()};
std::uniform_int_distribution<int> dist{1, 20};
};
因為這是 C++,所以一個不可破壞的線程 class 有點蹩腳。 Qt 在這方面保持QThread
不變,因為它可能會破壞事情(不是真的,但考慮到他們龐大的部署基礎,他們寧願在安全方面出錯)。
class Thread final : public QThread {
public:
using QThread::QThread;
~Thread() override {
requestInterruption();
quit();
wait();
}
};
Thread
可以隨時安全地銷毀,因此您可以按價值持有它。 此外, foo(void)
是 C 主義,在 C++ 中是不必要的(這只是噪音和單調,因為foo()
不像foo(...)
那樣在 C 中)。 現在事情變得相當簡單:
class InOutQueueTest: public QObject {
Q_OBJECT
Q_SLOT void limit15() {
Writer writer;
Thread thread; // must be last, after all the objects that live in it
connect(&writer, &Writer::haveEvents, &writer, &Writer::endPendings);
writer.start(); // now because we don't allow calling it from the wrong thread
writer.moveToThread(&thread);
QEventLoop eventLoop;
QTimer::singleShot(5000, &writer, [&]{
// This runs in the context of the writer, i.e. in its thread
writer.stop();
writer.moveToThread(eventloop.thread()); // needed to avoid a silly runtime warning
eventLoop.exit();
});
eventLoop.exec();
}
};
QTEST_GUILESS_MAIN( InOutQueueTest )
#include "inoutqueue.moc"
在刪除線程之前,您需要始終在線程上wait()
(在非 Qt 中說“加入”)。
在 QThread 上調用exit()
會立即返回並且不會等待。 並且deleteLater()
是在調用者的線程(不是目標線程)中執行的,所以你不能指望它被稱為“足夠晚”。
執行exit()
,然后wait()
,然后deleteLater()
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.