【问题标题】:QFuture that can be cancelled and report progress可以取消并报告进度的 QFuture
【发布时间】:2023-03-24 00:26:02
【问题描述】:

QFuture 类具有诸如cancel()progressValue() 等方法。这些显然可以通过QFutureWatcher 进行监控。但是,QtConcurrent::run() 的文档内容如下:

注意 QFuture 返回的 QtConcurrent::run() 不支持 取消、暂停或进度 报告。返回的 QFuture 可以 仅用于查询 运行/完成状态和返回 函数的值。

我徒劳地寻找实际上可以创建一个可以取消的QFuture 并报告单个长时间运行操作的进度的方法。 (看起来可能QtConcurrent::map() 和类似的函数可以,但我只有一个单一的、长时间运行的方法。)

(对于那些熟悉 .Net 的人,类似于 BackgroundWorker 类。)

有哪些选择?

【问题讨论】:

  • QFuture 似乎已部分实现。听起来他们正在寻找更详细的东西,并且很早就停止了。 KDE 人员添加了 ThreadWeaver:api.kde.org/frameworks-api/frameworks5-apidocs/threadweaver/…。然而,随着 std::thread 和 std::async 现在在标准(C++11)中,它们似乎是 QThread 和 QtConcurrentRun 的更好替代品。

标签: multithreading qt qtconcurrent qfuture


【解决方案1】:

虽然这个问题发布和回答已经有一段时间了,但我决定添加解决这个问题的方法,因为它与这里讨论的完全不同,我认为可能对其他人有用。首先,我的方法的动机是,当框架已经有一些成熟的类似物时,我通常不喜欢发明自己的 API。所以问题是:我们有一个很好的 API 来控制由 QFuture 表示的后台计算,但是我们没有支持某些操作的对象。好吧,让我们去做吧。查看 QtConcurrent::run 内部发生的事情会让事情变得更加清晰:制作了一个仿函数,将其包装到 QRunnable 中并在全局线程池中运行。

所以我为我的“可控任务”创建了通用接口:

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

最后是缺少的 runner 类,它将返回我们可控的 QFututres:

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

};

用户应该继承 ControllableTask,实现后台例程,它有时会检查传递给 run(TaskControl&) 的 TaskControl 实例的方法 shouldRun(),然后像这样使用它:

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

然后她可以通过调用 futureValue.cancel() 来取消它,记住取消是优雅的而不是立即的。

【讨论】:

  • 听起来和Threadweaver很像:api.kde.org/frameworks-api/frameworks5-apidocs/threadweaver/…你看过那个库吗?
  • @Atif:不,直到你指出我才知道。是的,它看起来很相似,但是 ThreadWeaver 看起来要复杂得多,如果我对这些任务应该如何相互依赖有要求,这可能是一个不错的选择。在我的具体情况下,我通过仅使用 Qt 编写了几十行代码来达到目标​​,并且不拉依赖就足够了。
  • 是的。 仅仅为了访问一个类而引入整个 KDE 是很疯狂的——不像这种紧凑的解决方案。如果您已经需要 KDE,那么 Threadweaver 可能是显而易见的选择。但是,在大多数情况下,您只想重新发明他们的轮子。 (什么?这是一个小轮子。
  • 虽然对于 C++ 层的人来说是崇高的,但遗憾的是,这个解决方案无法扩展到 Python。在低级模板和无偿使用私有的、未记录的 API(例如,QFutureInterfaceQFutureInterfaceBase)之间,Python 爱好者别无选择,只能在 QRunnable 子类中手动重新实现 QFuture API。 &lt;/sigh&gt;
  • @CecilCurry 事实上,KF5 库被设计为彼此独立工作,因此您将不会将“整个 KDE”拉入,而只是像任何其他库一样将一个简单的库拉入你的项目。它是对 ThreadWeaver only 的简单依赖。 ThreadWeaver 只依赖于 Qt。
【解决方案2】:

我不久前解决了这个精确的问题,并制作了一个名为“Thinker-Qt”的东西......它提供了一个名为 QPresentQPresentWatcher 的东西:

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

它仍然是 Alpha 版,我一直想回去修补它(并且需要尽快这样做)。我的网站上有一个幻灯片等。我还记录了如何改变 Mandelbrot 以使用它。

如果您想查看和/或贡献,它是开源和 LGPL。 :)

【讨论】:

    【解决方案3】:

    严的说法不准确。使用 moveToThread 是实现正确行为的一种方法,但它不是唯一的方法。

    另一种方法是覆盖 run 方法并创建要由那里的线程拥有的对象。接下来调用 exec()。 QThread 可以有信号,但要确保连接都是排队的。此外,对 Thread 对象的所有调用都应通过也通过 Queued 连接连接的插槽。或者,函数调用(将在调用者执行线程中运行)可以触发信号到线程拥有的对象(在 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() 方法中创建并在退出运行之前销毁。类析构函数应该只告诉线程退出并使用 QWaitCondition 等待线程实际完成执行。请注意,当这样做时,QThread 派生类在头文件中确实有 Q_OBJECT 宏,并且确实包含信号和槽。

    如果您愿意利用 KDE 库,另一个选择是 KDE 的Thread 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 更加明确并且总是做正确的事情(不管你如何将来自线程管理器的信号连接到其他类的插槽)。您可以看到我的线程中的循环检查原子变量以查看进程何时被取消,从而处理取消要求。

    【讨论】:

      【解决方案4】:

      对于长时间运行的单个任务,QThread 可能是您的最佳选择。它没有内置的进度报告或取消功能,因此您必须自己动手。但是对于简单的进度更新来说并不难。要取消任务,请检查可以从任务循环中调用线程设置的标志。

      需要注意的一点是,如果您覆盖 QThread::run() 并将您的任务放在那里,您将无法从那里发出信号,因为 QThread 对象不是在它运行的线程中创建的,您无法从中提取 QObject正在运行的线程。 issue 有一篇很好的文章。

      【讨论】:

      • 实际上,只要您不调用moveToThread(theQthread),QThread就会驻留在主线程中,并且可以从run函数发出信号,直到调用moveToThread。任何对象都驻留在创建它们的线程中,这就是为什么您需要调用 moveTooThread
      • 那不是真的,你可以使用 Qt::BlockingQueuedConnection 发回主线程
      • 不幸的是,QThread 是一个糟糕的抽象。对象本身将存在于父线程中。 QThread 中的 run 方法在新线程上运行。类中的信号将由父/主线程的事件循环处理。这似乎与您的预期相反,因此抽象不好。从 QThread 对象发出信号或槽的最严格的方法是直接使用 QMetaInvoke。使用 Queued 连接调用它,您应该得到可预测的行为。如有疑问,请使用 QThread::currentThreadId() 检查线程 ID
      【解决方案5】:

      改进@Hatter 回答以支持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();
          }
      };
      

      用户示例:

      #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(); });
      }
      

      【讨论】:

      • 你能说明如何暂停和恢复可取消的任务吗?顺便说一句,这种方法已经帮了我很多,但现在我需要找到一种方法来暂停和恢复任务。
      • @ShujaatAliKhan 当你需要恢复任务时,你应该持有一些在run Functor中使用的变量,然后用更新的变量重新启动Functor
      • 那么如果你想得到返回值,你应该使用QFuture&lt;T&gt;,T是你的结果类型,使用QFutureWatcher得到完成的结果。也许您需要检查任务是否完成或取消。 (我不确定QFutureWatcher::isCanceled() 是否有效。)
      猜你喜欢
      • 2017-03-08
      • 2015-03-18
      • 1970-01-01
      • 1970-01-01
      • 2020-07-04
      • 1970-01-01
      • 2012-01-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多