QtConcurrent in Qt6.4.x changed?
-
In my application I make use of QtConcurrent. Up to version 6.3.2 all was fine, but with 6.4.0 and 6.4.1 I run into an issue that is preventing me doing an upgrade.
In my application I load images into a gallery. That is the images are displayed as thumbnails in a grid. As the images are quit big (originals from a modern camera, so 50+ Mb each), and the amount of them that are to be displayed can be several hundreds, the loading is done on several threads, using the QtConcurrent module.
This works ok. If I load a whole folder with images, one can see the cpu-cores doing their jobs concurrently, with the images appearing on the screen as the loading progresses. That is, with Qt 6.3.2, the behaviour is as expected and as desired.When I build the same program with Qt 6.4.1 (and also with 6.4.0) the behaviour is different. In the background, the loading still is ongoing (lot of cpu-activity), but the images no longer appear gradually on the screen. It is only when the total job of loading and scaling all images is (almost) finished when the images appear on the screen, all in one go. So one is looking at a screen where nothing seems to happen, and all of a sudden (after 30-60 seconds, depending on the number of images, of course) there they are, in one go. Total elapsed time is about the same. I certainly do not want this behaviour and I don't want my users to be exposed to this behaviour, but I have no clue what the cause of this is or what I can do about it.
As I have only changed the Qt-version I tend to blame the latter, but of course that may be too short-sighted. Perhaps somebody has some ideas what might be the cause or what to do?
Below is some code, trimmed for the purpose of this forum.
void ImageLoadJob::execMT() { auto watcher = new QFutureWatcher<QPixmap>; // ... leaked ... // Connect the future watcher to the slots handling the pixmap loading. QObject::connect( watcher, &watchertype::resultReadyAt, [this, watcher]( int n ) { taskReady( watcher, n ); } ); QObject::connect( watcher, &watchertype::finished, [this, watcher]() { finished( watcher ); } ); // Launch the job watcher->setFuture( QtConcurrent::mapped( _taskvec, ImageLoader() ) ); }
Here _taskvec is a vector with (pointers to) objects that carry the pathname of the images. ImageLoader is a functor doing the loading for each of these elements.
After loading an image, the watcher's resultReady signal is emitted, which is connected to the taskReady() methjod below:void ImageLoadJob::taskReady( pwatchertype watcher, int num ) { static int count=0; // only for demo std::cerr << ++count << ". file " << __FILE__ << ":" << __LINE__ << std::endl; _taskvec[num]->onTaskReady( watcher->resultAt( num ) ); }
I noticed that with 6.3.2 the latter function is regularly being called, as the loading continues, whereas with 6.4.1 this only happens when all the individual task have (almost) completed. It is if all the treads wait for each other to complete, and then they all complete.
Any ideas are welcome.
-
Maybe add a debug output at the start and end of your ImageLoader routines to see if the tasks are getting ready in time or all at once at the end.
-
Maybe add a debug output at the start and end of your ImageLoader routines to see if the tasks are getting ready in time or all at once at the end.
@Christian-Ehrlicher Thanks for responding. I did as you suggested. But with 6.4.1, like in 6.3.2, the loading of all images just proceeds normally. With 6.3.2 the display keeps up with the loading. But with 6.4.1 the display of the images is held up somehow, only when all loading is done, the images are displayed seemingly all in one go.
-
Can you reproduce it in a smaller application? When I see it correct it should be able to merge this into a small main function which start the threads (which do a simple wait or similiar), wait for the completion of them and do some debug output.
-
Can you reproduce it in a smaller application? When I see it correct it should be able to merge this into a small main function which start the threads (which do a simple wait or similiar), wait for the completion of them and do some debug output.
@Christian-Ehrlicher After some experimentation, the following small example clearly shows the difference between 6.3.2 and 6.4.[01].
Running the code with either version, one can clearly see that the threads (which indeed do a simple wait, a bit randomised in length) proceed as expected. This is shown by the terminal output. However, with 6.4.1 the update of the GUI is delayed until the very end, whereas with 6.3.2 the process just goes on, displaying results in the GUI as they come. The 6.4.1 results is undesirable, as progress is not being displayed.Below I will give the code (Four files: main.cpp, MainWindow.h, UrlClass.h and Worker.h). As the issue is about the running, not about the final result, I also provide a small CMakeLists.txt.
In the main program, a MainWindow is opened with a QTextArea as the central area. A vector taskvec contains (pointers to) UrlClass objects.
After defining a QFutureWatcher, each of these objects is to be processed by a Worker, using QtConcurrent::mapped(...). The worker hosts a pointer to the textArea, so that progress can be monitored by connecting the appropriate signals to lambda expressions, writing results on this textarea.Now follows the code:
main.cpp:#include "MainWindow.h" #include "UrlClass.h" #include "Worker.h" #include <QApplication> #include <QFutureWatcher> #include <QtConcurrent/QtConcurrent> #include <vector> int main( int argc, char** argv ) { QApplication app( argc, argv ); auto mainWin = new MainWindow; mainWin->resize( 800, 600 ); const int N = 250; std::vector<UrlClass*> taskvec; taskvec.reserve( N ); for ( int id = 0; id < N; id++ ) { taskvec.push_back( new UrlClass( id ) ); // leaked... } auto watcher = new QFutureWatcher<int>; QObject::connect( watcher, &QFutureWatcher<int>::resultReadyAt, [mainWin]( int n ) { mainWin->taskDone( n ); } ); QObject::connect( watcher, &QFutureWatcher<int>::finished, [mainWin]() { mainWin->allDone(); } ); Worker worker( mainWin->_center ); watcher->setFuture( QtConcurrent::mapped( taskvec, worker ) ); mainWin->show(); return app.exec(); }
The class UrlClass is almost empty:
UrlClass.h:#ifndef URLCLASS_H #define URLCLASS_H class UrlClass { public: int _id; public: UrlClass( int id ) : _id( id ) {} }; #endif
The class MainWindow augments a QMainWindow:
MainWindow.h:#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QTextEdit> class MainWindow : public QMainWindow { public: QTextEdit* _center; public: MainWindow() : QMainWindow( nullptr, {} ) { _center = new QTextEdit( this ); setCentralWidget( _center ); } void taskDone( int n ) { _center->append( QString( "done: " ) + QString::number( n ) ); } void allDone() { _center->append( "FINISHED!"); } }; #endif
The worker, finally, reads:
Worker.h:#ifndef WORKER_H #define WORKER_H #include <QTextEdit> #include "UrlClass.h" #include <chrono> #include <iostream> #include <random> #include <thread> class Worker { private: QTextEdit* _textEdit; public: Worker( QTextEdit* textEdit ) : _textEdit( textEdit ) {} int operator()( const UrlClass* todo ) { static std::random_device device; static std::default_random_engine engine( device() ); static std::uniform_int_distribution<int> uniform( 250, 750 ); auto msec = uniform( engine ); std::cout << "doing: " << todo->_id << " (" << msec << ")\n"; std::this_thread::sleep_for( std::chrono::milliseconds( msec ) ); return msec; } }; #endif
To make it all run, here is a CMakelists.txt:
cmake_minimum_required(VERSION 3.16) project(qtconcurrentbug LANGUAGES CXX) set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Concurrent) FILE( GLOB sources "*.cpp" "*.h" ) qt_add_executable(bug246 ${sources}) target_link_libraries(bug246 PUBLIC Qt::Core Qt::Gui Qt::Widgets Qt::Concurrent )