QFuture, которое можно отменить и сообщить о прогрессе - PullRequest
13 голосов
/ 24 марта 2011

Класс QFuture имеет такие методы, как cancel(), progressValue() и т. Д. Их, по-видимому, можно отслеживать с помощью QFutureWatcher.Однако документация для QtConcurrent::run() гласит:

Обратите внимание, что QFuture, возвращаемое QtConcurrent :: run (), не поддерживает отмену, приостановку или создание отчетов о ходе выполнения.Возвращаемое QFuture может использоваться только для запроса статуса выполнения / завершения и возвращаемого значения функции.

Я тщетно искал, какой метод на самом деле может создатьQFuture, который можно отменить и сообщить о ходе выполнения одной длительной операции.(Похоже, что возможно QtConcurrent::map() и аналогичные функции могут, но у меня есть только один длительный метод.)

(Для тех, кто знаком с .Net, что-то вроде BackgroundWorker класса.)

Какие варианты доступны?

Ответы [ 4 ]

18 голосов
/ 24 мая 2013

Хотя прошло много времени с тех пор, как этот вопрос был опубликован и на него был дан ответ, я решил добавить свой способ решения этой проблемы, поскольку он довольно сильно отличается от того, что обсуждалось здесь, и я думаю, что он может быть полезен для кого-то еще.Во-первых, мотивация моего подхода заключается в том, что я обычно не люблю изобретать собственные API, когда у фреймворка уже есть несколько зрелых аналогов.Итак, проблема в том, что у нас есть хороший API для управления фоновыми вычислениями, представленными QFuture <>, но у нас нет объекта, который поддерживает некоторые операции.Ну, давай сделаем это.Взгляд на то, что происходит внутри QtConcurrent :: run, делает вещи намного понятнее: создается функтор, оборачивается в QRunnable и запускается в глобальном ThreadPool.

Поэтому я создал универсальный интерфейс для своих «управляемых задач»:

class TaskControl
{
public:
    TaskControl(QFutureInterfaceBase *f) : fu(f) {  }
    bool shouldRun() const { return !fu->isCanceled(); }
private:
    QFutureInterfaceBase *fu;
};

template <class T>
class ControllableTask
{
public:
    virtual ~ControllableTask() {}
    virtual T run(TaskControl& control) = 0;
};

Затем, следуя тому, что сделано в qtconcurrentrunbase.h, я сделал q-runnable для запуска задач такого типа (этот код в основном из qtconcurrentrunbase.h, но немного изменен):

template <typename T>
class RunControllableTask : public QFutureInterface<T> , public QRunnable
{
public:
    RunControllableTask(ControllableTask<T>* tsk) : task(tsk) { }
    virtial ~RunControllableTask() { delete task; }

    QFuture<T> start()
    {
        this->setRunnable(this);
        this->reportStarted();
        QFuture<T> future = this->future();
        QThreadPool::globalInstance()->start(this, /*m_priority*/ 0);
        return future;
    }

    void run()
    {
        if (this->isCanceled()) {
            this->reportFinished();
            return;
        }
        TaskControl control(this);
        result = this->task->run(control);
        if (!this->isCanceled()) {
            this->reportResult(result);
        }
        this->reportFinished();
    }

    T  result;
    ControllableTask<T> *task;
};

И, наконец, отсутствующий класс бегуна, который вернет нам управляемый QFututre <> s:

class TaskExecutor {
public:
    template <class T>
    static QFuture<T> run(ControllableTask<T>* task) {
        return (new RunControllableTask<T>(task))->start();
    }

};

Пользователь должен создать подкласс ControllableTask, реализовать фоновую подпрограмму, которая иногда проверяет метод shouldRun () экземпляра TaskControl, переданного в run (TaskControl &), а затем используйте его следующим образом:

QFututre<int> futureValue = TaskExecutor::run(new SomeControllableTask(inputForThatTask));

Затем она может отменить его, вызвав futureValue.cancel (), учитывая, что отмена является изящной, а не немедленной.

3 голосов
/ 01 ноября 2011

Я решил эту точную проблему некоторое время назад и создал нечто, называемое «Thinker-Qt» ... оно предоставляет нечто, называемое QPresent и QPresentWatcher:

http://hostilefork.com/thinker-qt/

Это все еще довольно альфа, и я намеревался вернуться и возиться с этим (и должен будет сделать это в ближайшее время). На моем сайте есть слайд-колода и тому подобное. Я также задокументировал, как можно изменить Мандельброта, чтобы использовать его.

Это открытый исходный код и LGPL, если вы хотите посмотреть и / или внести свой вклад. :)

2 голосов
/ 07 апреля 2011

Заявление Яна неточно.Использование moveToThread - это один из способов достижения правильного поведения, но это не единственный метод.

Альтернативой является переопределение метода run и создание ваших объектов, которые должны принадлежать этому потоку.Далее вы вызываете exec ().QThread может иметь сигналы, но убедитесь, что все соединения поставлены в очередь.Также все вызовы в объект Thread должны осуществляться через слоты, которые также подключены через соединение с очередями.В качестве альтернативы вызовы функций (которые будут выполняться в потоке выполнения вызывающих) могут инициировать сигналы для объектов, принадлежащих потоку (созданных в методе run), опять же соединения должны быть поставлены в очередь.

Одна вещь дляобратите внимание, что конструктор и деструктор работают в главном потоке выполнения.Строительство и очистка должны быть выполнены в бегах.Вот пример того, как должен выглядеть ваш метод run:

void MythreadDerrivedClass::run()
{
  constructObjectsOnThread();
  exec();
  destructObjectsOnThread();
  m_waitForStopped.wakeAll();
}

Здесь constructObjectsOnThread будет содержать код, который, как вы считаете, принадлежит конструктору.Объекты будут освобождены в destructObjectsOnThread.Фактический конструктор класса вызовет метод exit (), в результате чего exec () завершится.Обычно вы будете использовать условие ожидания, чтобы сидеть в деструкторе до тех пор, пока цикл не вернется.

MythreadDerivedClass::~MythreadDerivedClass()
{
  QMutexLocker locker(&m_stopMutex);
  exit();
  m_waitForStopped.wait(locker.mutex(), 1000);
}

Итак, конструктор и деструктор снова работают в родительском потоке.Объекты, принадлежащие потоку, должны быть созданы в методе run () и уничтожены перед выходом из run.Деструктор класса должен только сообщить потоку о выходе и использовать QWaitCondition, чтобы дождаться, пока поток фактически завершит выполнение.Обратите внимание, что когда все сделано таким образом, у производного класса QThread в заголовке есть макрос Q_OBJECT, и он содержит сигналы и слоты.

Другой вариант, если вы открыты для использования библиотеки KDE, - это поток KDE.Weaver .Это более полная реализация многозадачности, основанная на задачах, аналогичная QtConcurrentRun в том, что она использует пул потоков.Это должно быть знакомо любому, кто имеет опыт работы с Qt.

Тем не менее, если вы открыты для метода c ++ 11 и делаете то же самое, я бы посмотрел на std::async,Во-первых, вы больше не будете зависеть от Qt, но API также проясняет, что происходит.С классом MythreadDerivedClass, унаследованным от QThread, у читателя создается впечатление, что MythreadDerivedClass является потоком (так как он имеет отношение наследования) и что все его функции выполняются в потоке.Однако в потоке работает только метод run().std :: async проще в использовании и имеет меньше ошибок.Весь наш код в конечном итоге поддерживается кем-то другим, и подобные вещи имеют значение в долгосрочной перспективе.

C ++ 11 / w QT Пример:

class MyThreadManager {
  Q_OBJECT
public:
  void sndProgress(int percent)
  void startThread();
  void stopThread();
  void cancel() { m_cancelled = true; }
private:
  void workToDo(); 
  std::atomic<bool> m_cancelled;
  future<void> m_threadFuture;
};

MyThreadedManger::startThread() {
  m_cancelled = false;
  std::async(std::launch::async, std::bind(&MyThreadedManger::workToDo, this));
}

MyThreadedManger::stopThread() {
  m_cancelled = true;
  m_threadfuture.wait_for(std::chrono::seconds(3))); // Wait for 3s
}

MyThreadedManger::workToDo() {
  while(!m_cancelled) {
    ... // doWork
    QMetaInvoke::invokeMethod(this, SIGNAL(sndProgress(int)), 
      Qt::QueuedConnection, percentDone); // send progress
  }
}

По сути, то, что у меня здесь есть, не сильно отличается от того, как ваш код будет выглядеть с QThread, однако этоболее ясно, что в потоке работает только workToDo() и что MyThreadManager управляет только потоком, а не самим потоком.Я также использую MetaInvoke для отправки очередного сигнала для отправки наших обновлений прогресса с учетом требований к отчету о ходе выполнения.Использование MetaInvoke более явное и всегда делает правильно (не имеет значения, как вы подключаете сигналы от ваших потоковых менеджеров к слотам других классов).Вы можете видеть, что цикл в моем потоке проверяет атомарную переменную, чтобы увидеть, когда процесс отменяется, так что обрабатывает требование отмены.

1 голос
/ 24 марта 2011

Для длительной одиночной задачи QThread, вероятно, является вашей лучшей ставкой.Он не имеет встроенных отчетов о проделанной работе или отмены функций, поэтому вам придется свернуть свои собственные.Но для простого обновления прогресса это не так сложно.Чтобы отменить задачу, проверьте наличие флага, который можно установить из вызывающего потока в цикле вашей задачи.

Стоит отметить, что если вы переопределите QThread::run() и поместите свою задачу туда, вы не сможете подать сигналоттуда, поскольку объект QThread не создается в потоке, в котором он работает, и вы не можете извлечь QObject из запущенного потока.На эту проблему есть хорошая рецензия.

...