0

In my desktop software I'm starting several QtConcurrent threads from the main window. I've checked the thread IDs with QThread::currentThreadId() and noticed that they have the same ID as the GUI thread. Made some experiments, and noticed that the culprit is the waitForFinished() method. But in a quite strange way...

I wrote a minimal test, where I spawn a qtconcurrent on button click. The run method updates a counter on a spinbox.

#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QDebug>
#include <QThread>
#include <qtconcurrentrun.h>

MainWindow::MainWindow(QWidget *parent)
  : QMainWindow(parent)
  , ui_(new Ui::MainWindow)
  , cnt_(0)
{
  ui_->setupUi(this);
  connect(this, &MainWindow::setSpinBoxValue, ui_->spinBox, &QSpinBox::setValue);

  qDebug() << "MainWindow() thread ID: " << QThread::currentThreadId();
}

MainWindow::~MainWindow()
{
  qDebug() << "~MainWindow() thread ID: " << QThread::currentThreadId();

  delete ui_;
}

void MainWindow::on_pushButton_clicked()
{
  qDebug() << "on_pushButton_clicked() thread ID: " << QThread::currentThreadId();

  QFuture<void> th = QtConcurrent::run(this, &MainWindow::updateValue);

  // TEST MADE ENABLING/DISABLING THE FOLLOWING LINE
  th.waitForFinished();
}

void MainWindow::updateValue()
{
  qDebug() << "updateValue() thread ID: " << QThread::currentThreadId();

  emit setSpinBoxValue(++cnt_);
}

If I remove/comment th.waitForFinished() line the debug output shows different IDs for the click thread and the concurrent thread, as expected:

14:38:17: Debugging starts
MainWindow() thread ID:  0x1ad0
on_pushButton_clicked() thread ID:  0x1ad0
updateValue() thread ID:  0xc40
on_pushButton_clicked() thread ID:  0x1ad0
updateValue() thread ID:  0xc40
on_pushButton_clicked() thread ID:  0x1ad0
updateValue() thread ID:  0xc40
~MainWindow() thread ID:  0x1ad0
14:38:27: Debugging has finished

If I wait for thread termination (th.waitForFinished() called), the concurrent runs in a new thread the first time, then it runs on the main GUI thread on all successive call

14:40:23: Debugging starts
MainWindow() thread ID:  0x764
on_pushButton_clicked() thread ID:  0x764
updateValue() thread ID:  0x83c
on_pushButton_clicked() thread ID:  0x764
updateValue() thread ID:  0x764
on_pushButton_clicked() thread ID:  0x764
updateValue() thread ID:  0x764
~MainWindow() thread ID:  0x764
14:40:35: Debugging has finished

Why is this happening?

I suppose it has something to do with the fact that the waiting is done in the main thread. But once the wait is finished the software is free to create a new thread, so I don't know why this is happening.

I'm working on Windows 10, Qt 5.15.2 MSVC2019 x64

il_mix
  • 553
  • 8
  • 20
  • I am not sure why it is dong that however in Qt it is required to update the GUI from the GUI thread. Related: [https://stackoverflow.com/questions/14545961/modify-qt-gui-from-background-worker-thread](https://stackoverflow.com/questions/14545961/modify-qt-gui-from-background-worker-thread) – drescherjm Sep 06 '21 at 13:38
  • I've edited the code to be compliant (QSpinBox value setting via signal/slot). Didn't change QtConcurrent behavior. – il_mix Sep 06 '21 at 13:47

2 Answers2

0

It seems to be an unexpected behavior as it is not documented. Perhaps the key to understanding the cause of the problem is that by default QtConcurrent::run() uses QThreadPool::globalInstance() but if a different QThreadPool is created then this problem is not observed:

void MainWindow::on_pushButton_clicked()
{
  qDebug() << "on_pushButton_clicked() thread ID: " << QThread::currentThreadId();

  QThreadPool pool;
  QFuture<void> th = QtConcurrent::run(&pool, this, &MainWindow::updateValue);

  // TEST MADE ENABLING/DISABLING THE FOLLOWING LINE
  th.waitForFinished();
}
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Using a new QThreadPool will resolve the issue, indeed. Is it safe to declare a new pool every time? To create a new pool for every new thread sounds quite awful... It is quite strange that it is needed to create a new pool, instead of trusting Qt to create a new thread in the global thread pool. I also confirm that if I explicitly use the global instance `QtConcurrent::run(QThreadPool::globalInstance(), this, &MainWindow::updateValue)` i get the previously described behavior. I think I'll file a bug report on this. – il_mix Sep 07 '21 at 06:58
0

I had a chat at Qt bugreports on the topic.

The noticed behavior, althoug not documented, is expected. Summarizing:

  • QtConcurrent::run() is called
  • waiForFinished() is called
  • main thread need to stop witing for other thread to finish
  • but why bothering creting a new thread when the behavior is exactly like doing a function call? => Qt automatically converts QtConcurrent::run() in a function call (executed in main thread)

This won't happen if multiple threads are created

QFuture<void> th[2];
for(unsigned int i = 0; i < 2; i++)
  th[i] = QtConcurrent::run(this, &MainWindow::updateValue, i);
for(unsigned int i = 0; i < 2; i++)
  th[i].waitForFinished();

In this case, Qt can't convert QtConcurrent::run() in a function call, otherwise the 2 threads won't run in parallel. So 2 new threads will be created (both updateValue() functions will be run in threads with ID different from main thread's ID).

To fix the issue with the single thread, use a QFutureWatcher instead of waitForFinished(), that emits signal when the thread terminates; main thread won't be locked (will return on main thread when finished in a slot is connected to the signal), and new threads will be created on every QtConcurrent::run() call.

il_mix
  • 553
  • 8
  • 20