WonderTiger WonderTiger - 20 days ago 14
C++ Question

Qt blocked event loop

I recently started using the QT framework. Yesterday I began programming a simple multithreaded application. At the moment I'm somewhat stuck on the following problem.

Consider two worker classes that both use a thread to do some 'heavy computations'. The first class, FooWorker, looks like the following:

class FooWorker : public QObject
{
Q_OBJECT

public:
FooWorker() : QObject() { }
~FooWorker() { }

signals:
void notify(int);
void aborted();

public slots:
void doWork()
{
int counter = 0;
forever {

// For the sake of this example this reassembles a heavy computational process
if(counter++ < 10) {
emit notify(counter);
QThread::sleep(1);
} else {
counter = 0;

// Wait until we get a signal to restart the process
mutex_.lock();
condition_.wait(&mutex_);
mutex_.unlock();
}
// We should check for a cancellation flag every iteration...
}

emit aborted();
}

private:
QMutex mutex_;
QWaitCondition condition_;
};


The slot 'doWork' will be scheduled to run in another thread. The slot will run forever and is emitting a signal every second until 10 notifications are emitted. After that we wait until it is woken up again.

The second class, BarWorker, looks like this:

class BarWorker : public QObject
{
Q_OBJECT

public:
BarWorker() : QObject() { }
~BarWorker() { }

signals:
void aborted();

public slots:
void doWork()
{
forever {
// Another heavy computational process
QThread::sleep(1);

// We should check for a cancellation flag every iteration...
}

emit aborted();
}

void onNotify(int value)
{
qDebug() << "Notification value:" << value;
}
};


Again the slot 'doWork' will be scheduled to run in another thread. The slot will run forever to do a heavy computational process. Again once the process is done we will wait until it is woken up again (for the sake of this example I left that out in this class).

Finally the main looks like the following:

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

QThread* barThread = new QThread();
BarWorker* barWorker = new BarWorker();
barWorker->moveToThread(barThread);

QThread* fooThread = new QThread();
FooWorker* fooWorker = new FooWorker();
fooWorker->moveToThread(fooThread);

// Automatically deletes worker and thread
QObject::connect(fooThread, SIGNAL(started()), fooWorker, SLOT(doWork()));
QObject::connect(fooWorker, SIGNAL(aborted()), fooThread, SLOT(quit()));
QObject::connect(fooWorker, SIGNAL(aborted()), fooWorker, SLOT(deleteLater()));
QObject::connect(fooThread, SIGNAL(finished()), fooThread, SLOT(deleteLater()));

QObject::connect(barThread, SIGNAL(started()), barWorker, SLOT(doWork()));
QObject::connect(barWorker, SIGNAL(aborted()), barThread, SLOT(quit()));
QObject::connect(barWorker, SIGNAL(aborted()), barWorker, SLOT(deleteLater()));
QObject::connect(barThread, SIGNAL(finished()), barThread, SLOT(deleteLater()));

QObject::connect(fooWorker, SIGNAL(notify(int)), barWorker, SLOT(onNotify(int)), Qt::QueuedConnection);

fooThread->start();
barThread->start();

return a.exec();
}


When I run the application nothing gets printed. That was to be expected because the event loop of the BarWorker instance is blocked. As the 'notify' signal gets emitted the 'onNotify' slot is queued onto the event queue. Because we have a never ending loop (until we manually abort it) in the 'doWork' slot, the 'onNotify' slot will not be called. To solve this I can do a couple of things, namely:


  1. Connect the 'notify' signal to the 'onNotify' slot by using the Qt::DirectConnection flag. This way it looks like a normal function call executing on the signalling thread.

  2. Occasionally call the QCoreApplication::processEvents() method to force the event queue to be processed.

  3. Unknown solution I do not know at this time :)???



I hope someone has some alternative solution to this problem, or even suggest an entire different approach, because IMHO above solutions are somewhat ugly and do not feel right.

Answer

I don't think there is any "magic" solution to be found here; a thread can't be running Qt's event loop if it is running your own custom event loop. In practice, there are two common solutions, which are really two sides of the same coin:

  1. Call processEvents() periodically from your event loop, as you suggested in your question, so that the Qt event-handling code occasionally gets to run and handle incoming asynchronous signals.

  2. Don't have a long-running loop in your doWork() method. Instead, do a short amount of work, store the results/state of that work in a member variable or somewhere, and then call something like QTimer::singleShot(0, this, SLOT(doWork())) so that the Qt event loop will call your doWork() method again soon after the first call to doWork() returns. That way the Qt event loop never gets held off for longer than the (brief) period of time taken up by a single doWork() call.

Of those two options, I think the second is preferable, because it allows the Qt event loop to run in its normal fashion, and it also avoids a potential tripping-over-your-own-shoelaces issue -- e.g. imagine if while using solution (1) your call to processEvents() causes a slot to be called that deletes the BarWorker object. When the processEvents() call returns, BarWorker::doWork() will resume executing, but at that point, all of the local member variables and virtual methods it might access as part of its normal execution have been destroyed, and reading or writing them will cause undefined behavior (if you're lucky, an easy-to-debug crash). That possible snafu can't happen when using solution (2), since if the BarWorker object gets deleted between calls to doWork(), any queued-up asynchronous call to doWork() will be safely cancelled.