QT串口内存泄漏问题

3

我使用以下代码与USB串口设备通信:

#include "masterthread.h"
#include <QtSerialPort/QSerialPort>
#include <QTime>
#include "Windows.h"
#include "Psapi.h"
#include <QDebug>
QT_USE_NAMESPACE

MasterThread::MasterThread(QObject *parent)
: QThread(parent), waitTimeout(0), quit(false)
{
}

MasterThread::~MasterThread()
{
    mutex.lock();
    quit = true;
    cond.wakeOne();
    mutex.unlock();
    wait();
}

void MasterThread::run()
{
    bool currentPortNameChanged = false;

    QSerialPort serial;
    serial.setPortName("COM3");
    serial.setBaudRate(57600);
    serial.setStopBits(static_cast<QSerialPort::StopBits>(1));
    serial.setDataBits(static_cast<QSerialPort::DataBits>(8));
    serial.setParity(static_cast<QSerialPort::Parity>(0));
    serial.open(QIODevice::ReadWrite);

    //Tell the serial port connected device to start talking
    //--------------------------------------
    const char init[] = { 0x0d, 0x0d, 0x0d };
    serial.write(init, sizeof(init));
    const char* cmd = "mavlink stop\n";
    serial.write(cmd, strlen(cmd));
    serial.write(init, 2);
    cmd = "uorb start";
    serial.write(cmd, strlen(cmd));
    serial.write(init, 2);
    cmd = "sh /etc/init.d/rc.usb\n";
    serial.write(cmd, strlen(cmd));
    serial.write(init, 4);
    serial.waitForBytesWritten(100);

    int i = 0;
    int j = 0;
    forever
    {

        //Write test data out
        //-----------------------------
        QByteArray test(2000, 't');
        serial.write(test);
        bool check = serial.waitForBytesWritten(100);
        if (!check)
        {
            qDebug() << "FAIL: " << j++;
        }

        if (serial.waitForReadyRead(20))
        {
            QByteArray responseData = serial.readAll();
            while (serial.waitForReadyRead(10))
                responseData += serial.readAll();

            QString response(responseData);
            qDebug() << response;
        }
        QThread::msleep(20);

        //Print memory usage
        //---------------------------------------------------
        if (i++ % 10 == 0)
        {
            PROCESS_MEMORY_COUNTERS memcount;
            if (!GetProcessMemoryInfo(GetCurrentProcess(), &memcount, sizeof(memcount))) return;
            qDebug()<<"----------------------------" << memcount.WorkingSetSize / 1024 << "KB memory used";
        }
    } // end foever

    qDebug() << "Exiting forever loop";
}

使用以下简单的main.cpp文件:

#include <QApplication>
#include "masterthread.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    MasterThread thread;
    thread.start();
    return app.exec();
}

但内存使用量不断增加,每小时增加5~10MB,好像存在一些泄漏。

这个设备应该持续连接数天甚至数周……

我在Qt5.6下运行windows7 debug环境,我做错了什么吗?


2
如果您正在使用带有Qt消息循环的线程运行,则在调用QThread::msleep(20);之前最好调用QCoreApplication::processEvents(); - PeterT
2
@evilruff 如果一些Qt对象在内部使用deleteLater()或其他依赖于事件循环的机制,我可以看到它会导致这种行为。我不太了解Qt,无法确定哪些对象依赖于事件循环,哪些不依赖,但如果您有更多经验,请随时分享您的想法。 - PeterT
2
@Nyaruko,它似乎仍然使用其他东西,如在 QTimer 中内部使用异步信号,这可能仍然解释了内存问题,那只是一个例子。您是否意味着即使偶尔调用 QCoreApplication::processEvents();,显然的内存泄漏仍然存在? - PeterT
2
你没有必要重新实现一个线程来实现这段代码。重构方法:将所有内容放入一个 QObject 中,forever 部分应该包含在从零超时定时器调用的 slot/functor 中,然后摆脱 waitFor,改用信号/槽连接到 QIODevice 信号上。这将导致更清晰、更高效、更声明式的代码。可参考例如 这个答案那个答案 - Kuba hasn't forgotten Monica
1
你还没有表明你有内存泄漏的问题。你应该能够指出一个特定的分配,它从未被释放。GetProcessMemoryInfo并不能告诉你很多信息。每小时增加5~10MB可能是由于地址空间碎片化而不是泄漏所致。获取一个调试的Qt构建,并使用它与内存调试器一起使用。 - Kuba hasn't forgotten Monica
显示剩余10条评论
2个回答

2
许多Qt组件有隐式依赖于其事件循环的情况。虽然您通过调用app.exec();启动了主线程的事件循环,但您没有处理在QThread MasterThread thread;中创建的QObjects生成的事件。有关Qt中事件处理的详细信息和细微差别,请参见此页面:https://wiki.qt.io/Threads_Events_QObjects#Threads_and_QObjects。但解决方案归结为:如果您想要在处理某些长时间运行的任务的线程中处理排队的Qt事件,您应该定期调用QCoreApplication::processEvents();。这将防止Qt事件无休止地排队。

假设相应的线程没有任何槽附加到它上面,那么在run()中调用QCoreApplication::processEvents()通常是安全的吗? - Nyaruko
@Nyaruko 是的,processEvents 是线程安全的,如果调用线程没有 Qt 事件循环(即如果您正在使用原始的 std::thread 或其他不是 QThread 的东西),它将不执行任何操作。 - PeterT
但是如果我在没有事件循环的情况下使用QThread,这样可以吗?(我重新实现了run()函数) - Nyaruko
@Nyaruko 我不确定这是如何可能的。Qt会自动在你使用某些(或者也许是全部,我没有检查过)Qt对象或调用QThread::currentThread()时将线程“采纳”。你可以尝试手动使用moveToThread将你创建的所有对象移动到主线程的事件循环中。但是,你必须检查你正在使用的QObjects是否是线程安全的(因为它们的槽将在另一个线程中执行)。 - PeterT

0

在查看Qt 5.7、5.6、5.5的代码和阅读文档后进行了编辑。

由于已经有一个答案被接受了,我只想在这里添加一些想法,因为评论太长了。

保持简短——你接受的答案是错误的。

事情有两面性。由于SO答案通常被认为“只要它们能工作就可以”,我想解释一下自己的想法...

如果你查看提供的代码,那么没有任何问题。所有对象都被正确地堆栈分配,并应该自动销毁。

问题在于QtSerial使用deleteLater(),然后一个问题——如何正确删除这些分配。

如果任何模块/对象/代码使用deleteLater(),它需要一个事件循环,如果在没有事件循环的线程上调用deleteLater(),则对象将在线程终止后被删除。只要上面的代码没有运行事件循环,processEvents就不会起作用...实际上,processEvents()并不是用于这个的,因为整个想法是从被称为deleteLater()的上下文中返回并有一个next运行,这在Qt源代码中得到了检查,所以直接调用processEvent()而不增加循环计数将什么也不做,这就是为什么你接受的答案完全错误的原因。

结论:
如果任何对象需要事件循环运行,应该在文档中明确说明,因为在事件循环之外使用QIODevice同步模式并没有什么问题。
所以,在我看来,问题在于QT串口本身存在一个错误,建议您报告此问题。
总的来说,Qt运行永无止境的循环是非常错误的做法。更好、更干净的方法是使用QObject Worker策略,将其推入线程中,并有适当的事件循环运行等。
对于小型“线程化”任务,最好使用QtConcurrent。
正确的解决方法:
您将拥有一个具有正确运行事件循环和20ms定时器触发的线程,以执行您的任务。
// main thread:
class Worker: public QObject {
    public:
       Worker();

    public slots:

       onInit() {
          // initialize everything
          startTimer(20);
       }

    protected:

       void timerEvent(..) {
          // do your things every 20ms
       }   
  }

  ...

  QThread * pWorkerThread = new QThread();
  pWorkerThread->setObjectName(QString("Serial"));

  Worker * pWorker = new Worker();
  Worker->setObjectName(QString("Common Storage Impl"));
  Worker->moveToThread(WorkerThread);

  connect(pWorkerThread, SIGNAL(started()), pWorker, SLOT(onInit()));
  connect(pWorkerThread, SIGNAL(finished()), pWorker, SLOT(deleteLater()));
  connect(pWorkerThread, SIGNAL(finished()), pWorkerThread, SLOT(deleteLater()));

  pWorkerThread->start();

  ...

我同意,但你所说的副作用可能是什么?你能举个例子吗,这样我在进行这个临时修复时就可以避免它们。 - Nyaruko
好的,我改变了我的答案... 我认为应该是deleteLater(),因为我简要地看了一下,Serial使用QTimer,它可能很容易使用deleteLater(),但我仍然会将这个问题提交给Qt Bugtracker... 无论如何,processEvents根本不会销毁deleteLaters()。 - evilruff
正如我所写的,这非常非常依赖于Qt的版本。我试图解释为什么这是错误的方法,但使用与否取决于您自己。 - evilruff
例如,在我上面解释的设计中,实现流量整形要容易得多,因此总的来说,我同意 - 在99%的情况下,异步是正确的事件循环方式,但这并不意味着它在同步模式下使用时应该占用内存,否则应在文档中明确说明。 - evilruff
不,那不是 Kuba 的意思。他的意思是 QIODevice 基于异步环境和事件循环设计的类,但我认为这不是一种范式。你可以做你认为方便实用的事情。 - evilruff
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接