【问题标题】:Multi-threading design along with an undo/redo stack多线程设计以及撤消/重做堆栈
【发布时间】:2021-03-05 22:50:38
【问题描述】:

我有这个调用堆栈来执行繁重的计算:

// QML

StyledButton {
    onButtonClicked: {
        registeredCppClass.undoHandler.createCommand()
    }
}
void UndoHandler::createCommand()
{
    m_undoStack->push(new Command());
}
class Command : public QUndoCommand
{
public:
    Command();
    virtual ~Command();

    virtual void undo();
    virtual void redo();
    
    // ...
private:
    // Handler does the logic
    LogicHandler *m_logicHandler;
    // Output by logic handler
    QString m_outputName;
};
void Command::redo()
{
    
    if (/* */) {
        
    } else {
        // Run heavy computation
        m_outputName = m_logicHandler->run();
    }
}
QString LogicHandler::run()
{
    // Heavy computation starts
}

意图

我打算通过this approach 实现QThread,以防止在进行大量计算时GUI 变得无响应。但是我不知道QThreadWorker类需要在哪里实现。他们应该在:

  • UndoHandler::createCommand
  • Command::redo
  • LogicHandler::run
  • ... ?

考虑到信号槽连接,QThreadWorker 的最佳位置是什么?

【问题讨论】:

  • 由于UndoCommand 类拥有逻辑处理程序,因此在同一个类中启动一个新线程并将逻辑处理程序放入构造函数中的线程中对我来说是有意义的。不利的一面是,当命令破坏时(这可能经常发生),您必须调用线程 wait(),这可能需要一些时间。
  • 我将拥有一个系统范围的线程池并向其提交任务。

标签: c++ multithreading qt signals-slots qthread


【解决方案1】:

一般建议是永远不要阅读 QThread 文档。跟进从不阅读 Linux 线程文档。我是作为写过quite a few books on software development的人说的。

长的答案是线程在早期没有经过深思熟虑,并且那里有很多现在很糟糕的信息。在 Qt 3.x 期间,我相信早期的 Qt 4.x 应该从 QThread 派生一个类并覆盖 run()。您可以想象,这对于不熟悉一般线程并且无法在多线程中操作事物时设计互斥锁或其他访问保护的新手开发人员来说是多么有效。

您的设计让人觉得您已经阅读了本文档的部分内容。它仍然漂浮在那里。

在 Qt 4.x 的某个时候,我们不再应该从 QThread 派生。相反,我们应该只创建一个 QThread 和 moveToThread()。有点工作,但如果你的程序没有遵循代码中的快乐路径,你仍然可能最终得到“悬空线程”。

大约在同一时间,至少就我的曝光而言,我们还获得了一个全局线程池。

您的设计确实存在缺陷,因为您查看了旧文档。不是你的错。旧文档往往会首先出现在搜索中。

访问this GitHub repo并下拉项目。我完成的唯一 dev_doc 设置文档是针对 Fedora 的。如果我不被打扰,我今天早上将在 Ubuntu 上工作。 请务必查看钻石主题分支。

是的,这是使用 CopperSpice,但 CopperSpice 是 Qt 4.8 的一个分支,这是我能想到的唯一具体代码示例。您可以构建并运行编辑器,或者您可以通过阅读 advfind_busy.cpp 进行探索。您正在寻找的是如何使用 QFuture。该源文件只有大约 200 行长,而且它有一个短头文件。

扔掉你当前的设计。你需要 QFuture 和 QtConcurrent::run()。

注意:与当前的 Qt 5.x 相比,这些东西的头文件在名称和位置上有所不同。如果您选择继续使用 Qt,您将需要查找这些内容。 如何你使用这些东西不是。

注意 2: 如果您没有某种节流控制来将这些任务中的每一个限制为单个线程实例,您将需要动态创建和销毁 QFuture 对象。这意味着您必须有某种列表或向量来跟踪它们,并且您的对象析构函数需要遍历该列表来杀死线程并删除对象。

如果你想继续在 Ubuntu 上设置 CopperSpice,它分布在 multi-part blog post starting here

【讨论】:

  • 我同意 QFutureQtConcurrent 值得研究。一些注意事项:1)问题中提到的使用QThread 的方法没有子类化任何东西,这已经在正确的道路上。 2) 这个问题只给出了QUndoCommand 部分的实现,我想说那部分并不是“真正有缺陷的”。
  • LogicHandler::run() 最明确地表示从 QThread 派生的类。该问题特别指出“打算实施QThread”。在当前的 Qt 世界中,“实现 QThread”确实存在缺陷。真正需要看的部分是 LogicHandler 的实现。
  • 可以通过删除一些不相关的段落(CopperSpice?)并插入 QtConcurrent::run() 的实际代码示例来改进此答案。
  • 答案是完整的。 SO(和其他网站)上真正可悲的趋势之一是人们发布既不编译也不运行的代码 sn-ps。他们需要知道如何构建和运行它,这样他们才能充分发挥作用。在各种方法中添加 qDebug() 消息,这样他们就可以看到什么时候调用了什么以及调用了什么。获得完整的理解。假定构建环境的 ~5 行代码 sn-p 应始终被否决。开发人员需要功能齐全的代码。
【解决方案2】:

恕我直言,您的意图是正确的,并且您正朝着正确的方向前进(撇开使用 QtConcurrency 的论点——线程池和期货——因为这与当前的问题无关)。让我们解决第一部分:对象和执行流程。

由于代码 sn-ps 中已对这些类进行了概述,因此您需要格外小心以正确地将它们推送到线程边界。如果您想一想,工作对象是在调用线程中创建的,因此对象的一些成员也将在调用线程中创建。对于指针成员,这不会造成太大问题,因为您可以选择延迟创建这些对象,直到 封闭对象实例被创建并移动到工作线程。但是,嵌入对象是在构造对象时创建的。如果嵌入对象是从 QObject 派生的,则它将其线程亲缘关系设置为调用者线程。在这种情况下,信号将无法正常工作。为了缓解这个问题,通常最简单的方法是将工作线程传递给工作对象的构造函数,因此工作对象能够将其所有嵌入的对象移动到工作线程。

其次,假设如下:

  1. Command 拥有一个唯一的 LogicHandler 实例,并且
  2. LogicHandler 没有状态,并且
  3. LogicHandler 是 QObject 的子类,并且
  4. LogicHandler是工人阶级

我的建议是将线程的旋转放在Command::redo 中,然后连接类似于this article 底部给出的建议的信号。此外,您不会将Command.m_outputName 设置为LogicHandler::run 的返回值。 LogicHandler::run 应该返回无效。相反,您应该向LogicHandler 添加一个信号,该信号在完成处理后发出字符串值;然后,在Command 中添加一个插槽来处理它。一个 QString 可以很容易地跨线程边界进行编组(确保建立正确类型的连接,请参阅here)。

worker 启动方法连接到线程started 信号开始执行。无需从 QThread 继承并覆盖 run。 worker 还应该发出一个finished 信号,该信号应该连接到线程的quit 插槽。 worker 的finished 信号也应该连接到线程和worker 的deleteLater 槽。设置好这些后,只需调用线程的start 方法即可。

从那里,执行将从redo 返回,并且当它发出一个信号(我提到过你需要添加的那个)并传递输出字符串时,你会被通知工作完成。如果工作人员的生命周期与Command 的实例不同(我猜更长,因为您需要启动一个线程来执行长时间操作),那么您将需要连接从工作对象返回值信号到不同的对象。

【讨论】:

    猜你喜欢
    • 2021-06-04
    • 2023-03-10
    • 1970-01-01
    • 2017-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-21
    • 2016-03-31
    相关资源
    最近更新 更多