简体   繁体   中英

QThread: Destroyed while thread is still running in QTest

I have written a small multithreaded message queue. Now I am trying to do a test. But I am having a synchronization problem that I am not able to fix.

If I remove everything about the queue, the minimum verifiable example looks like this (sorry, it's a bit big):

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

I hope the test lasts 5 seconds, and ends correctly. However, I get:

WARNING: InOutQueueTest::limit15() Generate 19 numbers, starting at 517
Loc: [/home/juanjo/Trabajos/qml/test/inoutqueue.cpp(53)]
WARNING: InOutQueueTest::limit15() Generate 19 numbers, starting at 536
Loc: [/home/juanjo/Trabajos/qml/test/inoutqueue.cpp(53)]
QFATAL: InOutQueueTest::limit15() QThread: Destroyed while thread is still running
FAIL: : InOutQueueTest:.limit15() Received a fatal error.
Loc: [Unknown file(0)]
Totals: 1 passed, 1 failed, 0 skipped, 0 blacklisted, 5068ms
********* Finished testing of InOutQueueTest *********
Aborted

The code that should end the test (after 5 seconds) is this:

void onTimeout( ) {
    writer->stop( );
    backendThread->exit( 0 );
    backendThread->deleteLater( );

    stop = true;
}

I call the stop( ) method in Writer , call the exit( ) method in the auxiliary thread, and delete it in the next iteration of the event loop (it's the theory).

Too, in the Writer class, the stop( ) method is:

void stop( ) {
    m_inputTimer.stop( );
    m_outputTimer.stop( );
}

I simply do stop both timers.

  • What am I doing wrong?

  • How do I solve it?

I have written a small multithreaded message queue

Why? Qt inherently has one already... Post QEvent s to QObject s - it's done in a thread-safe manner, with support for event priorities, automatic draining when receivers are deleted, etc. There's even some support for custom allocators for QEvent , so you could use a dedicated memory pool if you wanted to. Of course a dedicated queue may be faster, but that requires benchmarks and some requirement for which Qt's own queue's performance is insufficient. Multithreaded message queues have pesky corner cases as the receivers traverse threads, etc. - there's a reason why Qt's event queue code is less than straightforward. It's not far fetched to imagine that eventually you'd be reimplementing all of it, so there better be a good reason:)


First, let's note that the includes can be much simpler - and there's never a need for the QtModule/QtInclude format - that only helps when the application build is misconfigured. Not doing that lets you know of the problem earlier in the build - otherwise it'd fail at link time. Thus:

#include <QtCore>
#include <QtTest>

The slots that only emit a signal are unnecessary: signals are invokable, and you can connect anything invokable to signals directly - in Qt 4 syntax. In modern Qt syntax, slots aren't really necessary unless you need them for metadata (eg as used by the QtTest to detect test implementations). Otherwise, connect signals to any method whatsoever, even in non-QObjects, or to a functor (eg a lambda).

Now the serious problems:

  1. The timers can only be manipulated from the thread they are in.

    1. Move the timers to the same thread their user object is in. This is best done by letting the parent object own the timers - the moving then happens automatically. Note that QObject ownership in no way clashes with retaining the timers by value - there'll be no double deletion, since by the time the parent QObject begins to delete the children, the timers are long gone.

    2. Assert that the methods that manipulate the timers are called from the proper thread.

    3. Optionally provide convenience to allow propagating the calls across the thread barriers.

  2. You were re-connecting the timers to their slots each time the writer was started. That's never correct. Such connections should be made according to the scope of the objects involved, ensuring that they only happen once. In this case, since the timers live as long as the enclosing object, the connections belong in the constructor.

#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};
};

Ssince this is C++, a thread class that's not destructible is a wee bit lame. Qt has kept QThread unchanged in that regard since it'd potentially break things (not really, but they'd rather error on the safe side given their enormous deployed base).

class Thread final : public QThread {
public:
  using QThread::QThread;
  ~Thread() override {
    requestInterruption();
    quit();
    wait();
  }
};

The Thread can be safely destroyed at any time, and thus you can hold it by value. Also, foo(void) is a C-ism, unnecessary in C++ (it's just noise and unidiomatic since foo() is not foo(...) like it was in C). Now things become rather simple:

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"

You need to always wait() on a thread before deleting it (in non-Qt speak "join").

Calling exit() on a QThread returns immediately and does not wait. And deleteLater() is performed in the caller's thread (not target thread), so you cannot count on it to be called "late enough".

Do exit() , then wait() , then deleteLater() .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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