开发者

QFuture that can be cancelled and report progress

开发者 https://www.devze.com 2023-02-20 02:53 出处:网络
The QFuture class has methods such as cancel(), progressValue(), etc. These can apparently be monitored via a QFutureWatcher. However, the documentation for QtConcurrent::run() reads:

The QFuture class has methods such as cancel(), progressValue(), etc. These can apparently be monitored via a QFutureWatcher. However, the documentation for QtConcurrent::run() reads:

Note that the QFuture returned by QtConcurrent::run() does not support canceling, pausing, or progress reporting. The QFuture returned can only be used to query for the running/finished status and the return value of the function.

I have开发者_运维知识库 looked in vain for what method actually can create a QFuture that can be cancelled and report progress for a single long-running operation. (It looks like maybe QtConcurrent::map() and similar functions can, but I just have a single, long-running method.)

(For those familiar with .Net, something like the BackgroundWorker class.)

What options are available?


Though it's been a while since this question was posted and answered I decided to add my way of solving this problem because it is rather different from what was discussed here and I think may be useful to someone else. First, motivation of my approach is that I usually don't like to invent own APIs when framework already has some mature analogs. So the problem is: we have a nice API for controlling background computations represented by the QFuture<>, but we have no object that supports some of the operations. Well, let's do it. Looking on what's going on inside QtConcurrent::run makes things much clearer: a functor is made, wrapped into QRunnable and run in the global ThreadPool.

So I created generic interface for my "controllable tasks":

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;
};

Then, following what is made in qtconcurrentrunbase.h I made q-runnable for running this kind of tasks (this code is mostly from qtconcurrentrunbase.h, but slightly modified):

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;
};

And finally the missing runner class that will return us controllable QFututre<>s:

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

};

The user should sublass ControllableTask, implement background routine which checks sometimes method shouldRun() of TaskControl instance passed to run(TaskControl&) and then use it like:

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

Then she may cancel it by calling futureValue.cancel(), bearing in mind that cancellation is graceful and not immediate.


I tackled this precise problem a while ago, and made something called "Thinker-Qt"...it provides something called a QPresent and a QPresentWatcher:

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

It's still fairly alpha and I've been meaning to go back and tinker with it (and will need to do so soon). There's a slide deck and such on my site. I also documented how one would change Mandelbrot to use it.

It's open source and LGPL if you'd like to take a look and/or contribute. :)


Yan's statement is inaccurate. Using moveToThread is one way of achieving the proper behavior, but it not the only method.

The alternative is to override the run method and create your objects that are to be owned by the thread there. Next you call exec(). The QThread can have signals, but make sure the connections are all Queued. Also all calls into the Thread object should be through slots that are also connected over a Queued connection. Alternatively function calls (which will run in the callers thread of execution) can trigger signals to objects that are owned by the thread (created in the run method), again the connections need to be Queued.

One thing to note here, is that the constructor and destructor are running in the main thread of execution. Construction and cleanup need to be performed in run. Here is an example of what your run method should look like:

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

Here the constructObjectsOnThread will contain the code one would feel belongs in the constructor. The objects will be deallocated in destructObjectsOnThread. The actual class constructor will call the exit() method, causing the exec() to exit. Typically you will use a wait condition to sit in the destructor till the run has returned.

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

So again, the constructor and destructor are running in the parent thread. The objects owned by the thread must be created in the run() method and destroyed before exiting run. The class destructor should only tell the thread to exit and use a QWaitCondition to wait for the thread to actually finish execution. Note when done this way the QThread derived class does have the Q_OBJECT macro in the header, and does contain signals and slots.

Another option, if you are open to leveraging a KDE library, is KDE's Thread Weaver. It's a more complete task based multitasking implementation similar QtConcurrentRun in that it leverages a thread pool. It should be familiar for anyone from a Qt background.

That said, if you are open to a c++11 method of doing the same thing, I would look at std::async. For one thing, you will no longer have any dependance on Qt, but the api also makes more clear what is going on. With MythreadDerivedClass class inheriting from QThread, the reader gets the impression that MythreadDerivedClass is a thread (since it has an inheritance relationship), and that all its functions run on a thread. However, only the run() method actually runs on a thread. std::async is easier to use correctly, and has fewer gotcha's. All our code is eventually maintained by someone else, and these sorta things matter in the long run.

C++11 /w QT Example:

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

Basically, what I've got here isn't that different from how your code would look like with QThread, however, it is more clear that only workToDo() is running on the thread and that MyThreadManager is only managing the thread and not the thread itself. I'm also using MetaInvoke to send a queued signal for sending our progress updates with takes care of the progress reporting requirement. Using MetaInvoke is more explicit and always does the right thing (doesn't matter how you connect signals from your thread managers to other class's slots). You can see that the loop in my thread checks an atomic variable to see when the process is cancelled, so that handles the cancellation requirement.


Improve @Hatter answer to support Functor.

#include <QFutureInterfaceBase>
#include <QtConcurrent>

class CancellationToken
{
public:
    CancellationToken(QFutureInterfaceBase* f = NULL) : m_f(f){ }
    bool isCancellationRequested() const { return m_f != NULL && m_f->isCanceled(); }
private:
    QFutureInterfaceBase* m_f;
};

/*== functor task ==*/
template <typename T, typename Functor>
class RunCancelableFunctorTask : public QtConcurrent::RunFunctionTask<T>
{
public:
    RunCancelableFunctorTask(Functor func) : m_func(func) { }
    void runFunctor() override
    {
        CancellationToken token(this);
        this->result = m_func(token);
    }
private:
    Functor m_func;
};

template <typename Functor>
class RunCancelableFunctorTask<void, Functor> : public QtConcurrent::RunFunctionTask<void>
{
public:
    RunCancelableFunctorTask(Functor func) : m_func(func) { }
    void runFunctor() override
    {
        CancellationToken token(this);
        m_func(token);
    }
private:
    Functor m_func;
};

template <class T>
class HasResultType
{
    typedef char Yes;
    typedef void *No;
    template<typename U> static Yes test(int, const typename U::result_type * = 0);
    template<typename U> static No test(double);
public:
    enum { Value = (sizeof(test<T>(0)) == sizeof(Yes)) };
};

class CancelableTaskExecutor
{
public:
    //function<T or void (const CancellationToken& token)>
    template <typename Functor>
    static auto run(Functor functor)
        -> typename std::enable_if<!HasResultType<Functor>::Value,
                        QFuture<decltype(functor(std::declval<const CancellationToken&>()))>>::type
    {
        typedef decltype(functor(std::declval<const CancellationToken&>())) result_type;
        return (new RunCancelableFunctorTask<result_type, Functor>(functor))->start();
    }
};

User example:

#include <QDateTime>
#include <QDebug>
#include <QTimer>
#include <QFuture>
void testDemoTask()
{
    QFuture<void> future = CancelableTaskExecutor::run([](const CancellationToken& token){
        //long time task..
        while(!token.isCancellationRequested())
        {
            qDebug() << QDateTime::currentDateTime();
            QThread::msleep(100);
        }
        qDebug() << "cancel demo task!";
    });
    QTimer::singleShot(500, [=]() mutable { future.cancel(); });
}


For a long running single task, QThread is probably your best bet. It doesn't have build-in progress reporting or canceling features so you will have to roll your own. But for simple progress update it's not that hard. To cancel the task, check for a flag that can be set from calling thread in your task's loop.

One thing to note is if you override QThread::run() and put your task there, you can't emit signal from there since the QThread object is not created within the thread it runs in and you can't pull the QObject from the running thread. There is a good writeup on this issue.

0

精彩评论

暂无评论...
验证码 换一张
取 消