繁体   English   中英

QThread:当线程仍在 QTest 中运行时被销毁

[英]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)。

现在严重的问题:

  1. 定时器只能从它们所在的线程中操作。

    1. 将计时器移动到其用户 object 所在的同一线程。这最好通过让父 object拥有计时器来完成 - 然后移动会自动发生。 请注意, QObject所有权与按值保留计时器绝不会发生冲突——不会有双重删除,因为当父QObject开始删除子项时,计时器早已不复存在。

    2. 断言操作定时器的方法是从正确的线程调用的。

    3. 可选地提供方便以允许跨线程屏障传播调用。

  2. 每次启动编写器时,您都会将计时器重新连接到它们的插槽。 那永远是不正确的。 此类连接应根据所涉及对象的 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM