Разорвать петлю из другого потока - PullRequest
0 голосов
/ 24 ноября 2018

В настоящее время у меня есть два класса, которые выглядят примерно так:

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

Итак, идея заключается в том, что в потоке Worker работает цикл, который можно остановить из потока Controller.

В этом примере это делается путем вызова QCoreApplication::processEvents(), который позволяет сигналам вызывать слоты перед возвратом управления в цикл.
Важно, чтобы цикл останавливался только в начале или конце итерации..

Хотя это хорошо работает, я думаю, QCoreApplication::processEvents() довольно дорого, по крайней мере, когда используется внутри очень длинного цикла (до тысячи на практике).

Так что мой вопросесть, как я могу добиться того же результата лучше / дешевле?

1 Ответ

0 голосов
/ 25 ноября 2018

В настоящее время мне известны три альтернативных решения.

1.QThread::requestInterruption (предложено @Felix)

Согласно QThread::isInterruptionRequested:

Старайтесь не называть это слишком часто, чтобы снизить накладные расходы.

Принимая во внимание, что QCoreApplication::processEvents не делает никаких замечаний по поводу производительности или использования памяти, поэтому я не думаю, что QThread::requestInterruption является улучшением по сравнению с QCoreApplication::processEvents в этом случае.


2.std::atomic (предложено @Felix)

Основная характеристика атомарных объектов состоит в том, что доступ к этому содержавшемуся значению из разных потоков не может вызывать гонки данных [...]

Логическое значение может храниться в std::atomic, который можно сделать членом класса Controller вместо класса Worker.Затем нам нужно передать ссылку на aborted и сохранить ее в Worker, а при необходимости установить true из Controller.

Я не полностью протестировал этот подход, поэтомуПожалуйста, поправьте меня, если я что-то не так понял.

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

Требуется логическое значение paused.Controller и Worker требуется доступ для чтения / записи к нему.

Установите paused в true в Controller при необходимости.
Во время цикла в Worker, if(paused): QWaitCondition::wait() до тех пор, пока из вызывающего потока не будет вызван QWaitCondition::wakeAll().
QMutex::lock необходимо будет вызывать при каждом обращении к paused.

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
...