bur
bur

Reputation: 754

Breaking a loop from another thread

Currently I have two classes that look something like this:

class Worker : public QObject
{
    Q_OBJECT

    bool aborted = false;

public:
    Worker() : QObject() {}

public slots:
    void abort() { aborted = true; }

    void doWork()
    {
        while(!aborted && !work_finished)
        {
            //do work
            QCoreApplication::processEvents();
        }
    }
};

class Controller : public QObject
{
    Q_OBJECT

    QThread workerThread;
public:
    Controller() : QObject()
    {
        Worker *worker = new Worker;
        worker->moveToThread(&workerThread);
        connect(&workerThread, &QThread::finished, worker, &Worker::deleteLater);
        connect(this, &Controller::startWork, worker, &Worker::doWork);
        connect(this, &Controller::aborted, worker, &Worker::abort);
    }

signals:
    void startWork();
    void aborted();
};

Controller *cont = new Controller;
emit cont->startWork(); // Start the loop
emit cont->aborted(); // Stop the loop

So the idea is that there is a loop running in a Worker thread, which can be stopped from a Controller thread.

In the example this is done by calling QCoreApplication::processEvents(), which allows signals to call slots before returning control to the loop.
It's important the loop is only stopped at the start or end of an iteration.

Although this works nicely, I think QCoreApplication::processEvents() is pretty expensive, at least when used inside a very long loop (up to thousands in practice).

So my question is, how can I achieve the same result in a better/cheaper way?

Upvotes: 1

Views: 513

Answers (1)

bur
bur

Reputation: 754

There are three alternative solutions that I'm aware of at this time.

1. QThread::requestInterruption (suggested by @Felix)

According to QThread::isInterruptionRequested:

Take care not to call it too often, to keep the overhead low.

Whereas QCoreApplication::processEvents makes no remark on performance or memory usage, so I don't think QThread::requestInterruption is an improvement over QCoreApplication::processEvents in this case.


2. std::atomic (suggested by @Felix)

The main characteristic of atomic objects is that access to this contained value from different threads cannot cause data races [...]

The boolean can be stored inside a std::atomic which can be made a member of the Controller class instead of the Worker class. Then we need to pass a reference to aborted to and store it in Worker, and set it to true from Controller when needed.

I didn't fully test this approach, so please correct me if I got something wrong.

class Worker : public QObject {
    Q_OBJECT
    std::atomic<bool> &aborted;
public:
    Worker(std::atomic<bool> &aborted) : QObject(), aborted(aborted) {}
public slots:
    void doWork() {
        while(!aborted.load() && !work_finished) /* do work */
    }
};

class Controller : public QObject {
    Q_OBJECT
    QThread workerThread;
    std::atomic<bool> aborted;
public:
    Controller() : QObject() {
        aborted.store(false);
        Worker *worker = new Worker(aborted);
        worker->moveToThread(&workerThread);
        connect(&workerThread, &QThread::finished, worker, &Worker::deleteLater);
        connect(this, &Controller::startWork, worker, &Worker::doWork);
        connect(this, &Controller::aborted, worker, &Worker::abort);
    }

    void abort() { aborted.store(true); }
signals:
    void startWork();
};

Controller *cont = new Controller;
emit cont->startWork(); // Start the loop
cont->abort(); // Stop the loop

3. QWaitCondition & QMutex

A boolean paused will be needed. Controller and Worker need read/write access to it.

Set paused to true in Controller when needed.
During the loop in Worker, if(paused): QWaitCondition::wait() until QWaitCondition::wakeAll() is called from the calling thread.
QMutex::lock will need to be called whenever paused is accessed.

class Worker : public QObject {
    Q_OBJECT
    bool &aborted, &paused;
    QWaitCondition &waitCond;
    QMutex &mutex;
public:
    Worker(bool &aborted, bool &paused, QWaitCondition &waitCond, QQMutex &mutex)
        : QObject(), aborted(aborted), paused(paused), waitCond(waitCond), mutex(mutex) {}
public slots:
    void doWork() {
        while(!aborted && !work_finished) {
            //do work
            mutex.lock();
            if(paused) {
                waitCond.wait(&mutex);
                paused = false;
            }
            mutex.unlock();
        }
    }

    void abort() { aborted = true; }
};

class Controller : public QObject {
    Q_OBJECT
    bool aborted=false, paused=false;
    QWaitCondition waitCond;
    QMutex mutex;
    QThread workerThread;
public:
    Controller() : QObject() {
        Worker *worker = new Worker(aborted, paused, waitCond, mutex);
        worker->moveToThread(&workerThread);
        connect(&workerThread, &QThread::finished, worker, &Worker::deleteLater);
        connect(this, &Controller::startWork, worker, &Worker::doWork);
    }
    void abort() {
        mutex.lock();
        paused = true; // Worker starts waiting
        mutex.unlock();

        if(confirmed_by_user) aborted = true; // Don't need to lock because Worker is waiting
        waitCond.wakeAll(); // Worker resumes loop
    }
signals:
    void startWork();
};

Controller *cont = new Controller();
emit cont->startWork(); // Start the loop
cont->abort(); // Stop the loop

Upvotes: 1

Related Questions