Thursday, September 12, 2013

Trials and tribulations of not freezing your Qt event loop via a lengthy destructor.

I ran into an issue not too long ago where the application that I was writing needed to allocate a huge number of very tiny data structures and then delete them all at once. Now, obviously, there's something wrong with my program's design that it needs to accomplish this... but the point of this particular program was to solve a very specific problem without spending a huge amount of time on development.

The problem here was that even though the performance of the program was acceptable in all other respects, deleting all of that data took a long time. It took so long that the eventloop froze, and the rest of the program locked up until it was finished.

Here's my quick 15 minute solution. Absolutely do not use this technique in any mission critical code... but for a quick one-off, well, it's your project.

 Firstly, to save on having to manually manage cleaning up after myself, I initially create the data containers as a QSharedPointer
QSharedPointer<QStandardItemModel> data(new QStandardItemModel);


Because my data was being read from disk and the data structures created on another thread (so as to avoid freezing the gui), I was already moving the data containers from that alternate thread into the main thread using

data->moveToThread(QApplication::instance()->thread());

 From there, the QSharedPointer's are returned to the main thread as the contents of QFutures using the QtConcurrent module
QFutureWatcher<QSharedPointer<QStandardItemModel> > * future = new QFutureWatcher<QSharedPointer<QStandardItemModel> >;
future->setFuture(QtConcurrent::run(dataParserClass, dataParserFunction);

Which runs the parsing function on the parsing object that has been set up previously, on another thread to avoid blocking the event loop. Ultimately returning, through the future, the QSharedPointer to the container that we've re-assigned to the main thread.

Now, create a thread to hold the containers you wish to asynchronously delete

QThread * deleterThread = new QThread;
deleterThread->start();

Define a new object to hold the shared data, probably put this in a .h file:
class DataContainer : public QObject
{
Q_OBJECT
public:
explicit DataContainer()
: QObject()
{
}

QSharedPointer<QStandardItemModel> data;
public slots:
void populate(QSharedPointer<QStandardItemModel> var)
{
data = var;
emit populated();
}
signals:
void populated();
};


Then move the data to this new thread:
DataContainer * container = new DataContainer();
container->moveToThread(deleterThread);
connect(deleterThread, &QThread::finished, container, &DataContainer::deleteLater);
And finally put your data onto the new thread when it's finished being generated. Due to strange behavior of cross-thread signals, i ended up settling on this method of calling the function... but i rewrote enough of the rest of the code that this might not be necessary anymore. Who knows.

QMetaObject::invokeMethod(container, "populate",
Q_ARG(QSharedPointer<QStandardItemModel>, future->result()));

Now your data is referenced by the datacontainer object, which will keep your QSharedPointer active until you stop the deleterThread with deleterThread->quit(). The deletion will happen on the deleter thread, preventing your eventloop from freezing!

Like I said initially. This is a complete architectural hack, and you should avoid it, but sometimes we all make compromises for the sake of expediency.

No comments:

Post a Comment