簡體   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