Question

I have a class that should run in a thread and needs an event loop for the slots, currently I run it nicely with moveToThread(), but I'd like to use QThreadPool and I have encountered a problem.

When run with QThreadPool the run() method of my runnable is called from a pooled thread (I check this with QThread::currentThread()), but my slots aren't running in the pooled thread, so I think the object isn't moved to a thread in the pool.

I think this because I know the slots are run in the receiver's thread, which is exactly the (correct) behaviour I get when using the moveToThread() method and a QThread.

How do I get my QRunnable (Foo in the example below) to be run entirely in the pooled threads? Or is it something I'm doing wrong or understood wrong?

The following POC demonstrates the problem:

foo.h

#ifndef FOO_H
#define FOO_H

#include <QObject>
#include <QRunnable>
#include <QEventLoop>

class Foo : public QObject, public QRunnable
{
    Q_OBJECT
public:
    explicit Foo(int data, QObject *parent = 0);
    void run();
signals:
    void startWorking();
public slots:
    void doWork();

private:
    QEventLoop eventLoop;
    int data;
};

#endif // FOO_H

foo.cpp

#include "foo.h"

#include <QThread>
#include <QDebug>

Foo::Foo(int d, QObject *parent) :
    QObject(parent), eventLoop(this), data(d)
{
}

void Foo::run()
{
    qDebug() << "run() in: " << QThread::currentThread();
    connect(this, SIGNAL(startWorking()), this, SLOT(doWork()));
    emit startWorking();
    eventLoop.exec();
}

void Foo::doWork()
{
    qDebug() << "doWork() in: " << QThread::currentThread();
}

main.cpp

#include <QCoreApplication>
#include <QThreadPool>

#include "foo.h"

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

    Foo *foo = new Foo(42);

    QThreadPool::globalInstance()->start(foo);

    return a.exec();
}

Please note, however, that in my real code the signal won't be emitted right away, because it will be after I receive some data on the network.

PS: The POC can also be found here.

Was it helpful?

Solution

Maybe you could split your logic in class Foo into two:
the hoster QRunnable with a QEventLoop, and a worker QObject, which you create on the worker thread in run() before calling QEventLoop::exec method. Then you forward all the signals to the worker object. So now your slots will be called on the pooled thread.
However, QThreadPool is designed for executing lots of short tasks without creating too many simultaneous threads. Some tasks are enqueued and are waiting for others to finish. If this is not your intention, you might want to go back to good old QThread and use it instead.

OTHER TIPS

You can support both modes but it will require some coordination from the outside. My strategy is to emit a signal from inside QRunnable::run passing the current thread. When you plan to use it in a thread pool, use a Qt::BlockingQueuedConnection on this signal and do your moveToThread there. Otherwise, move it to the QThread and emit a signal to start working as usual.

TaskRunner.h

#pragma once

#include <QObject>
#include <QRunnable>
#include <QThread>

class TaskRunner : public QObject, public QRunnable
{
  Q_OBJECT
public:
  TaskRunner(int data, QObject* parent = nullptr);

  void run() override;

Q_SIGNALS:

  void start();
  void starting(QThread*);
  void stop();

private:
  int data;
};

TaskRunner.cpp

#include "TaskRunner.h"
#include <QEventLoop>
#include <stdexcept>


TaskRunner::TaskRunner(int data, QObject* parent)
: QObject(parent), data(data)
{
    // start should call run in the associated thread
    QObject::connect(this, &TaskRunner::start, this, &TaskRunner::run);
}

void TaskRunner::run()
{
    // in a thread pool, give a chance to move us to the current thread
    Q_EMIT starting(QThread::currentThread());

    if (thread() != QThread::currentThread())
      throw std::logic_error("Not associated with proper thread.");

    QEventLoop loop;
    QObject::connect(this, &TaskRunner::stop, &loop, &QEventLoop::quit);
    // other logic here perhaps
    loop.exec();
}

main.cpp

#include <QCoreApplication>
#include <QThreadPool>

#include "TaskRunner.h"

// comment to switch
#define USE_QTHREAD

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

    auto runner = new TaskRunner(42);

#ifdef USE_QTHREAD
    // option 1: on a QThread
    auto thread = new QThread(&a);
    runner->moveToThread(thread);
    QObject::connect(thread, &QThread::finished, runner, &QObject::deleteLater);
    Q_EMIT runner->start();

    // stop condition not shown
#else
    // option 2: in a thread pool
    QObject::connect(
      runner, &TaskRunner::starting, 
      runner, &QObject::moveToThread,
      Qt::BlockingQueuedConnection);
    QThreadPool::globalInstance()->start(runner);

    // stop condition not shown
#endif

    return a.exec();
}

Since the your connect call

connect(this, SIGNAL(startWorking()), this, SLOT(doWork()));

used the default parameter for connection type, it will be a Qt::Autoconnection. The signal is emitted from the pooled thread, and the slot still belongs to foo, which has a thread affinity to the main thread. The autoconnection will decide to put the slot in the event queue of the main thread.

There are two ways you can fix this:

1.

connect(this, SIGNAL(startWorking()), this, SLOT(doWork()), Qt::DirectConnection);

and remove the eventloop.exec();

2.

in the run method, move the foo object to the current thread before connecting the signal and slot.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top