Параллельная программа QThread создает утечку памяти при выходе из приложения - PullRequest
0 голосов
/ 25 ноября 2018

У меня большой проект с графическим интерфейсом, и я хочу управлять некоторыми файлами в фоновом режиме.Я реализовал новый поток для этой задачи, и во время выполнения все прекрасно работает.Но как только я закрываю приложение, visual-leak-detector обнаруживает 3-7 утечек памяти.

Я отделил свой многопоточный код и создал новый проект, чтобы проверить это на примере минимального кода, но я все еще не могучтобы исправить мою проблему.

Я думаю, что это как-то связано с циклом событий основной программы.Возможно, цикл не обрабатывает последние события для удаления моего класса потока и самого потока.Потому что я остановился и вышел из потока в деструкторе.Но я не уверен в этом.

Вот мой минимальный код: threadclass.hpp:

#include <QObject>
#include <QDebug>

class ThreadClass : public QObject {
    Q_OBJECT

public:
    explicit ThreadClass() {}
    virtual ~ThreadClass(){
        qDebug() << "ThreadClass Destructor";
    }

signals:
    // emit finished for event loop
    void finished();

public slots:
    // scan and index all files in lib folder
    void scanAll(){
        for(long i = 0; i < 10000; i++){
            for (long k = 0; k < 1000000; k++);
            if(i%500 == 0)
                qDebug() << "thread: " << i;
        }
    }
    // finish event loop and terminate
    void stop(){
        // will be processed after scanall is finished
        qDebug() << "STOP SIGNAL --> EMIT FINSIHED";
        emit finished();
    }
};

threadhandler.hpp:

#include <QObject>
#include <QThread>
#include "threadclass.hpp"

class ThreadHandler : public QObject {
    Q_OBJECT

public:
    explicit ThreadHandler(QObject *parent = 0) : parent(parent), my_thread(Q_NULLPTR) {}

    virtual ~ThreadHandler() {
        // TODO Check!
        // I think I don't have to delete the threads, because delete later
        // on finish signal. Maybe I just have to wait, but then how do I
        // check, if thread is not finished? Do I need to make a bool var again?

        if (my_thread != Q_NULLPTR && my_thread->isRunning())
        {
            emit stopThread();
            //my_thread->quit();
            my_thread->wait();
            //delete my_thread;
        }

        qDebug() << "ThreadHandler Destructor";
        my_thread->dumpObjectInfo();
    }

    void startThread(){
        if (my_thread == Q_NULLPTR)
        {
            my_thread = new QThread;
            ThreadClass *my_threaded_class = new ThreadClass();
            my_threaded_class->moveToThread(my_thread);

            // start and finish
            QObject::connect(my_thread, &QThread::started, my_threaded_class, &ThreadClass::scanAll);
            QObject::connect(this, &ThreadHandler::stopThread, my_threaded_class, &ThreadClass::stop);

            // finish cascade
            // https://stackoverflow.com/a/21597042/6411540
            QObject::connect(my_threaded_class, &ThreadClass::finished, my_threaded_class, &ThreadClass::deleteLater);
            QObject::connect(my_threaded_class, &ThreadClass::destroyed, my_thread, &QThread::quit);
            QObject::connect(my_thread, &QThread::finished, my_thread, &QThread::deleteLater);

            my_thread->start();
        }
    }

signals:
    void stopThread();

private:
    QObject *parent;
    QThread* my_thread;
};

Основной.cpp дрянной, но, похоже, достаточно хорошо имитирует поведение моей основной программы:

#include <QCoreApplication>
#include <QTime>
#include <QDebug>
#include "threadhandler.hpp"

#include <vld.h>

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    ThreadHandler *th = new ThreadHandler();
    th->startThread();

    // wait (main gui programm)
    QTime dieTime= QTime::currentTime().addSecs(5);
    while (QTime::currentTime() < dieTime) {
        QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
    }

    qDebug() << "DELETE TH";
    delete th;
    qDebug() << "FINISH ALL EVENTS";
    QCoreApplication::processEvents(QEventLoop::AllEvents, 500);
    qDebug() << "QUIT";
    QCoreApplication::quit();
    qDebug() << "CLOSE APP";
    // pause console
    getchar();
//    return a.exec();
}

Вот вывод из VLD:

WARNING: Visual Leak Detector detected memory leaks!
...
turns out this is very boring and uninteresting
...
Visual Leak Detector detected 3 memory leaks (228 bytes).
Largest number used: 608 bytes.
Total allocations: 608 bytes.
Visual Leak Detector is now exiting.
The program '[8708] SOMinimalExampleThreads.exe' has exited with code 0 (0x0).

Обновление 1: Я добавил qDebug() << "ThreadClass Destructor"; к деструктору ThreadClass, и мой новый вывод выглядит так:

...
thread:  9996
thread:  9997
thread:  9998
thread:  9999
ThreadHandler Destructor
FINISH ALL EVENTS
CLOSE APP

Теперь ясно, что деструктор моего многопоточного класса никогда не вызывается и поэтому теряется внедействительным.Но тогда почему это не работает?

QObject::connect(my_threaded_class, &ThreadClass::finished, my_threaded_class, &ThreadClass::deleteLater);

Обновление 2: Я нашел одну проблему в ThreadHandler:

emit stopThread();
my_thread->quit(); // <-- stops the event loop and therefore no deletelater
my_thread->wait();

Я удалил my_thread->quit() и теперьвызывается деструктор ThreadClass, но my_thread->wait() никогда не заканчивается.

Ответы [ 2 ]

0 голосов
/ 26 ноября 2018

Описание проблемы:

Когда деструктор ThreadHandler испускает stopThread из основного потока, Qt вызывает подключенный слот (&ThreadClass::stop), отправляя событие в цикл событий рабочего потока (иначесоединение в очереди).Это означает, что цикл обработки событий работника должен быть готов к приему новых событий, когда этот сигнал испускается (поскольку вы полагаетесь на него для выполнения надлежащей очистки).Однако, как вы уже заметили, вызов thread->quit() может привести к завершению цикла событий раньше, чем нужно (до того, как рабочий поток сможет вызвать ThreadClass::stop, и, следовательно, сигнал ThreadClass::finished не будет излучен).,Возможно, вы захотите изучить выходные данные этого минимального примера, который воспроизводит поведение, о котором я говорю:

#include <QtCore>

/// lives in a background thread, has a slot that receives an integer on which
/// some work needs to be done
struct WorkerObject : QObject {
  Q_OBJECT
public:
  using QObject::QObject;
  Q_SLOT void doWork(int x) {
    // heavy work in background thread
    QThread::msleep(100);
    qDebug() << "working with " << x << " ...";
  }
};

/// lives in the main thread, has a signal that should be connected to the
/// worker's doWork slot; to offload some work to the background thread
struct ControllerObject : QObject {
  Q_OBJECT
public:
  using QObject::QObject;
  Q_SIGNAL void sendWork(int x);
};

int main(int argc, char *argv[]) {
  QCoreApplication a(argc, argv);

  QThread thread;
  WorkerObject workerObj;
  workerObj.moveToThread(&thread);
  // quit application when worker thread finishes
  QObject::connect(&thread, &QThread::finished, &a, &QCoreApplication::quit);
  thread.start();

  ControllerObject controllerObj;
  QObject::connect(&controllerObj, &ControllerObject::sendWork, &workerObj,
                   &WorkerObject::doWork);

  for (int i = 0; i < 100; i++) {
    QThread::msleep(1);
    // call thread.quit() when i is equal to 10
    if (i == 10) {
      thread.quit();
    }
    controllerObj.sendWork(i);
  }
  return a.exec();
}

#include "main.moc"

На моей машине возможный вывод:

working with  0  ...
working with  1  ...
working with  2  ...
working with  3  ...

Обратите внимание, чтонесмотря на то, что thread.quit() вызывается на десятой итерации из основного потока, рабочий поток может не обработать все сообщения, полученные до вызова выхода (и мы получим значение 3 как последнее значение, обработанное рабочим). *

Решение:

На самом деле Qt предоставляет канонический способ выхода из рабочего потока и выполнения необходимой очистки, поскольку обрабатывается сигнал QThread::finished особым образом :

Когда этот сигнал испускается, цикл событий уже остановлен.Больше событий не будет обрабатываться в потоке, , кроме отложенных событий удаления . Этот сигнал может быть подключен к QObject :: deleteLater (), чтобы освободить объекты в этом потоке.

Это означает, что вы можете использовать thread->quit() (так же, как выделать), но вам просто нужно добавить:

connect(my_thread, &QThread::finished, my_threaded_class, &ThreadClass::stop);

к вашему startThread и удалить ненужные emit stopThread(); из деструктора.


* Я не смог найти ни одной страницы в документации, которая бы подробно объясняла это поведение, поэтому я привел этот минимальный пример, чтобы объяснить, о чем я говорю.

0 голосов
/ 25 ноября 2018

Я обнаружил, что функция my_thread->wait() блокирует цикл обработки событий, и поэтому каскад выхода и deleteLater никогда не завершается. Я исправил это с помощью другого метода ожидания готового потока:

removed

Вот недавно реализованное решение от Майка.Это было довольно легко реализовать, мне нужно было только изменить соединение на my_threaded_class::stop в классе threadhandler.

#include <QObject>
#include <QThread>
#include "threadclass.hpp"

class ThreadHandler : public QObject {
    Q_OBJECT

public:
    explicit ThreadHandler(QObject *parent = 0) : parent(parent), my_thread(Q_NULLPTR) {}

    virtual ~ThreadHandler() {

        if (my_thread != Q_NULLPTR && my_thread->isRunning())
        {
            my_thread->quit();
            my_thread->wait();
        }

        qDebug() << "ThreadHandler Destructor";
    }

    void startThread(){
        if (my_thread == Q_NULLPTR)
        {
            my_thread = new QThread;
            ThreadClass *my_threaded_class = new ThreadClass();
            my_threaded_class->moveToThread(my_thread);

            // start and finish
            QObject::connect(my_thread, &QThread::started, my_threaded_class, &ThreadClass::scanAll);
            // https://stackoverflow.com/questions/53468408
            QObject::connect(my_thread, &QThread::finished, my_threaded_class, &ThreadClass::stop);

            // finish cascade
            // https://stackoverflow.com/a/21597042/6411540
            QObject::connect(my_threaded_class, &ThreadClass::finished, my_threaded_class, &ThreadClass::deleteLater);
            QObject::connect(my_threaded_class, &ThreadClass::destroyed, my_thread, &QThread::quit);
            QObject::connect(my_thread, &QThread::finished, my_thread, &QThread::deleteLater);

            my_thread->start();
        }
    }

signals:
    void stopThread();

private:
    QObject *parent;
    QThread* my_thread;
};
...