PDA

View Full Version : Multithreaded loading of imaes with QRunnable



donelron
17th December 2015, 09:44
Dear all,

I am working with legacy code in a class AddTileStackToPixmapcacheWorker with header (much simplified example):




#ifndef AddTileStackToPixmapcacheWorkerQRunnableQRUNNABLE_ H__
#define AddTileStackToPixmapcacheWorkerQRunnableQRUNNABLE_ H__

#include <QObject>
#include <QRunnable>
#include <QGraphicsPixmapItem>

#include <boost/gil/extension/io/jpeg_io.hpp>
#include <boost/gil/extension/io/png_io.hpp>
#include <boost/gil/image.hpp>


#include <set>


class AddTileStackToPixmapcacheWorker : public QObject, public QRunnable {

Q_OBJECT
public:
AddTileStackToPixmapcacheWorker (QString Filename);
~AddTileStackToPixmapcacheWorker ();

void run();
void LoadImagesFromHardDriveIntoVector();
bool LoadImageDataRgb8(QString fileNameRelative, QPixmap & ima);

private:
QString m_Filename;
};

#endif


and cpp:

#include "AddTileStackToPixmapcacheWorker.h"

#include <iostream>
#include <QDebug>
#include <QThread>

AddTileStackToPixmapcacheWorker::AddTileStackToPix mapcacheWorker (QString Filenames) :
m_Filename (Filenames)
{
}

AddTileStackToPixmapcacheWorker::~AddTileStackToPi xmapcacheWorker()
{
}

void AddTileStackToPixmapcacheWorker::run()
{
QPixmap ima;
bool b = LoadImageDataRgb8(m_Filename, ima);
}



bool AddTileStackToPixmapcacheWorker::LoadImageDataRgb8 ( QString fileNameRelative, QPixmap & ima )
{
if (fileNameRelative.size())
{
QString fileNameAbsolute = "D:/Sharing/MicroscopyImageData/falc_Nr2_13013792_2015.04.02-12.32.10/images/" + fileNameRelative;
const char * pChar;


//check ending for .jpg or .jpeg
if (fileNameAbsolute.endsWith(".jpg", Qt::CaseInsensitive) || fileNameAbsolute.endsWith(".jpeg", Qt::CaseInsensitive))
{
pChar = "JPG";
}

ima.load(fileNameAbsolute, pChar);
return true;
}

return false;
}



I use it like that:


#include "AddTileStackToPixmapcacheWorker.h"

void myThreadingExample()
{

std::vector<QString> vecStrings;
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_0_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_0_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_1_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_1_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_10_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_10_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_11_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_11_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_12_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_12_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_13_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_13_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_14_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_14_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_15_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_15_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_16_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_16_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_17_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_17_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_18_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_18_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_19_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_19_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_2_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_2_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_20_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_20_Index_0_orig.jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_21_Index_0_ds(1).jpg"));
vecStrings.push_back(QString("img_Rev_1_Mag_2.5_Stack_21_Index_0_orig.jpg"));


for (int i=0; i< vecStrings.size(); i++)
{
AddTileStackToPixmapcacheWorker *pAddTileStackToPixmapcacheWorker = new AddTileStackToPixmapcacheWorker(vecStrings[i]);
pAddTileStackToPixmapcacheWorker->setAutoDelete(true);
QThreadPool::globalInstance()->setExpiryTimeout(-1);
QThreadPool::globalInstance()->start(pAddTileStackToPixmapcacheWorker);
}
}



int main(int argc, char *argv[])
{
QApplication app(argc, argv);
myThreadingExample();
return app.exec();
}


When I run this i get stuff like the following in the console:

QObject::startTimer: Timers cannot be started from another thread
QObject::startTimer: Timers cannot be started from another thread
QObject::startTimer: Timers cannot be started from another thread
QObject::startTimer: Timers cannot be started from another thread
QObject::startTimer: Timers cannot be started from another thread
QObject::startTimer: Timers cannot be started from another thread
QObject::startTimer: Timers cannot be started from another thread
QObject::startTimer: Timers cannot be started from another thread

and when I close my console window I get:


QObject::~QObject: Timers cannot be stopped from another thread
QWaitCondition: Destroyed while threads are still waiting
QWaitCondition: Destroyed while threads are still waiting
QWaitCondition: Destroyed while threads are still waiting
QWaitCondition: Destroyed while threads are still waiting
QWaitCondition: Destroyed while threads are still waiting
QWaitCondition: Destroyed while threads are still waiting
QWaitCondition: Destroyed while threads are still waiting
QWaitCondition: Destroyed while threads are still waiting

Once or twice (out of around 20 or 30 tries) it also crashed with some unhandled exception, but I don't remember what the exact error description was.

What am I doing wrong?
How else would I be able to load several images asynchronously?!?
I've tried to use only one "extra" thread for loading images, which is more stable, but for my use case is also much slower than the example in this post. So, to summarize: I am looking for either:
1) a way to fix my QRunnable example or
2) a different multithreaded approach that works
Any ideas?!? Many many thanks in advance!!!!!

anda_skoa
17th December 2015, 10:25
Load into a QImage, QPixmap is a GUI resource and can only be used from the main thread.

Why is your runnable also a QObject?
What do you do with the image once it is loaded?
Do you know all filenames beforehand or do you get new filenames during runtime?

Cheers,
_

donelron
17th December 2015, 16:11
Thanks for the quick answer!!!


Why is your runnable also a QObject?
This is because in the "real project" I also need signal/slot mechanism for the class.


What do you do with the image once it is loaded?
I convert it to QGraphicsPixmapItem, so I can add it to a QGraphicsScene with QGraphicsScene::addItem.


Do you know all filenames beforehand or do you get new filenames during runtime?
I get them at run time

anda_skoa
17th December 2015, 17:56
This is because in the "real project" I also need signal/slot mechanism for the class.

Hmm.
QObjects and thread pool are a bit problematic due to thread affinity of objects. The "owner thread" of the runnable will be the thread that created it, but it will be handled and deleted by the threadpool thread.

If all you do is emit a signal you should be fine though.



I convert it to QGraphicsPixmapItem, so I can add it to a QGraphicsScene with QGraphicsScene::addItem.

So you emit a signal with the loaded image and then in the receiver slot create a pixmap item to display it?



I get them at run time
Sorry, I wasn't clear.
Do you get the full list in one go or do you get files added by some form of events?

Cheers,
_

donelron
18th December 2015, 10:23
So you emit a signal with the loaded image and then in the receiver slot create a pixmap item to display it?
Yes, that's how it works (currently). However, I am not sure if the signal/slot approach unnecessarily slows down everything?!? Maybe the whole processing pipeline from loading images to displaying them in the scene could be faster with a "regular" function call instead of emitting a signal?!?...


Do you get the full list in one go or do you get files added by some form of events?
I get it by "some form of event": Whenever the application needs to display a new image a function is called that instantiates an AddTileStackToPixmapcacheWorker object. So, in contrast to my MWE the "real code" rather looks like:


void AddImageInViewer(ImagePointerClass * pIamAPointerThatUniquelyIdentifiesAnImage)
{
AddTileStackToPixmapcacheWorker *pAddTileStackToPixmapcacheWorker = new AddTileStackToPixmapcacheWorker(pIamAPointerThatUn iquelyIdentifiesAnImage);
pAddTileStackToPixmapcacheWorker->setAutoDelete(true);
QThreadPool::globalInstance()->setExpiryTimeout(-1);
QThreadPool::globalInstance()->start(pAddTileStackToPixmapcacheWorker);}

anda_skoa
18th December 2015, 11:51
Yes, that's how it works (currently). However, I am not sure if the signal/slot approach unnecessarily slows down everything?!?

You have to somehow transfer the data between threads.
Putting the images into queue with propery locking might or might not be faster.

The main advantage would be potentially getting more than one image per queue read.

Maybe the whole processing pipeline from loading images to displaying them in the scene could be faster with a "regular" function call instead of emitting a signal?!?...




I get it by "some form of event": Whenever the application needs to display a new image a function is called that instantiates an AddTileStackToPixmapcacheWorker object.

Ah, ok.
For the case of a one-time full list I would have suggested a dedicated loader thread.





void AddImageInViewer(ImagePointerClass * pIamAPointerThatUniquelyIdentifiesAnImage)
{
AddTileStackToPixmapcacheWorker *pAddTileStackToPixmapcacheWorker = new AddTileStackToPixmapcacheWorker(pIamAPointerThatUn iquelyIdentifiesAnImage);
pAddTileStackToPixmapcacheWorker->setAutoDelete(true);
QThreadPool::globalInstance()->setExpiryTimeout(-1);
QThreadPool::globalInstance()->start(pAddTileStackToPixmapcacheWorker);}

Auto delete is true by default.
I would also recommend to use a dedicated threadpool unless you are sure you are not needing the global instance anywhere else.

Cheers,
_

donelron
18th December 2015, 14:28
I would also recommend to use a dedicated threadpool unless you are sure you are not needing the global instance anywhere else.

Which disadvantage would result from re-using the global instance somewhere else?

anda_skoa
18th December 2015, 15:55
Which disadvantage would result from re-using the global instance somewhere else?

These runnables here are I/O bound and would potentially block runnables from other use cases.

A dedicated threadpool could even be restricted to fewer threads as to not cause to many parallel I/O operations.
E.g. the software might run on a system with traditional harddisks not with SSDs.

Cheers,
_

donelron
21st December 2015, 22:32
@anda_skoa:
I've been working on this a bit more and it seems that my minimal working example does not cover all the pitfalls of the "real code". The things that made it crash originate somwhere else.
So, I ended up, basically using it like I first posted, and included some of the modifications you proposed. Thanks once again!