1

I have a multithreaded application written in C++ with Qt. Currently, my application works by having a QThread instance (I have NOT subclassed QThread) which uses the default run() implementation that just calls QThread's exec() method, which provides an event dispatch loop.

I call moveToThread on certain QObject subclasses which actually perform the work I want done in a separate thread. I tell these objects to do work using Qt's signals/slots mechanism. I stop the thread gracefully by informing my worker objects to stop their work, and then calling quit() and wait() on my thread. This all works very nicely.

However, I now want to implement the following scenario:

  • The user clicks e.g. the "X" button on my application, because they want to close it.
  • I don't want any new work to be started, so I pause the event dispatch thread. If the current event being dispatched continues running, that's fine.
  • I prompt the user, allowing them to either a) discard all remaining jobs and exit (using quit() andwait()` - this already works), or b) don't exit the application, but instead continue working (resume the thread).

My problem is that QThread doesn't seem to have a pause() method. I've seen various examples online which add one (like the answers to this question). The problem is that these examples depend on having a custom run() implementation, and implementing pause there. Since I'm relying on QThread's event dispatch loop, these solutions won't work. I've considered doing something like reimplementing exec() or creating my own subclass of QAbstractEventDispatcher, but these solutions seem like a whole lot of work to get simple pause / resume functionality.

What's the easiest way to pause QThread's event dispatch loop (preventing it from dispatching any new events, but letting the current event continue)?

Community
  • 1
  • 1
CmdrMoozy
  • 3,870
  • 3
  • 19
  • 31
  • What if you just re-`start` the thread after `quit`. Wouldn't it work? – Lol4t0 Jul 27 '14 at 18:12
  • I for some reason was under the impression that it wouldn't, but I now don't see any reason to believe that it wouldn't from the docs. I'll try it. :) – CmdrMoozy Jul 27 '14 at 18:14

1 Answers1

1

I tried out the method suggested in the comments, but it took a bit of screwing around to get it to work totally correctly, so here's what I ended up with:

I subclassed QThread to add two new methods: pause and resume. There were a few things that needed to be dealt with delicately:

  1. Calling start() while the thread is still running does nothing. Since resume() might be called before the thread's existing job stops running, we need to do the actual resume in a slot connected to the thread's finished() signal.
  2. The finished() signal may be emitted just before the thread actually stops. Because of this, we need to call wait() in our slot before calling start().
  3. If resume() is called after the thread is already stopped, simply setting state variables won't work, because finished() will never be emitted. Because of this, we need to deal with that case by having non-signal-related resume code in the resume() method as well.

Here's the final product.

pausablethread.h:

#ifndef INCLUDE_PAUSABLE_THREAD_H
#define INCLUDE_PAUSABLE_THREAD_H

#include <QThread>

class QMutex;

class PausableThread : public QThread
{
    Q_OBJECT

    public:
        PausableThread(QObject *parent = 0);
        virtual ~PausableThread();

        void pause();
        void resume();

    private:
        QMutex *controlMutex;
        bool paused;
        bool resumeScheduled;

    private Q_SLOTS:
        void doResume();
};

#endif

pausablethread.cpp:

#include "pausablethread.h"

#include <QMutex>
#include <QMutexLocker>

PausableThread::PausableThread(QObject *parent)
    : QThread(parent), paused(false), resumeScheduled(false)
{
    controlMutex = new QMutex(QMutex::NonRecursive);

    QObject::connect(this, SIGNAL(finished()),
        this, SLOT(doResume()));
}

PausableThread::~PausableThread()
{
    delete controlMutex;
}

void PausableThread::pause()
{
    QMutexLocker locker(controlMutex);

    if(paused)
        return;

    paused = true;

    quit();
}

void PausableThread::resume()
{
    QMutexLocker locker(controlMutex);

    if(!paused)
        return;

    if(resumeScheduled)
        return;

    if(isFinished())
    {
        start();

        paused = false;
        resumeScheduled = false;
    }
    else
    {
        resumeScheduled = true;
    }
}

void PausableThread::doResume()
{ /* SLOT */

    QMutexLocker locker(controlMutex);

    if(!resumeScheduled)
        return;

    paused = false;
    resumeScheduled = false;

    wait();
    start();
}

This seems to work, mostly. I believe there are some potential race conditions if the thread happens to finish or start at the same time execution is inside resume() or doResume() in a different thread. It's not exactly clear to me how to solve this.

I tried something like overriding the superclass's start() slot with the following:

void start(Priority priority)
{
    QMutexLocker locker(controlMutex);

    QThread::start(priority);
}

The problem with this is that start() never actually returns until the thread finishes, so it never releases its lock on the mutex. Thus, when doResume() tries to acquire a lock, a deadlock is encountered, and the thread isn't successfully resumed.

I think what is really needed is a mutex which is exclusively locked any time the thread's running state is changed, but it isn't clear to me how to implement this.

Regardless, the window for this race to occur is very small,and this works "well enough" to answer my question. Thanks to @Lol4t0 for the suggestion!

CmdrMoozy
  • 3,870
  • 3
  • 19
  • 31