[英]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.