【问题标题】:Keep app responsive during long task在长时间任务期间保持应用程序响应
【发布时间】:2010-09-30 23:51:20
【问题描述】:

我们应用程序中的某个表单显示模型的图形视图。用户可以在大量其他内容中启动模型转换,这可能需要相当长的时间。这种转换有时会在没有任何用户交互的情况下进行,有时则需要频繁的用户输入。除非需要用户输入,否则应禁用 UI(仅显示进度对话框)。

可能的方法:

  1. 忽略这个问题,只需将转换代码放在一个过程中并调用它。不好,因为在转换需要一些时间但不需要用户输入的情况下,应用似乎挂起。
  2. 在代码中添加回调:这很突兀 - 您必须在转换代码中放置 很多 此类调用 - 而且不可预测 - 您永远无法确定自己会找到了合适的地点。
  3. 在代码中添加 Application.ProcessMessages:与回调相同的问题。此外,您还会遇到 ProcessMessages 的所有问题。
  4. 使用线程:这将我们从 2. 和 3 的“突兀和不可预测”部分中解脱出来。但是,由于用户输入需要“编组” - 调用 Synchronize,将任何定制记录中所需的参数等。调试也是一场噩梦,容易出错。

//编辑:我们当前的解决方案是线程。然而,由于用户输入,a** 很痛苦。在很多例程中可能会有很多输入代码。这让我觉得线程不是正确的解决方案。

我要让自己难堪,并发布我所生成的 GUI 和工作代码的邪恶组合的大纲:

type
  // Helper type to get the parameters into the Synchronize'd routine:
  PGetSomeUserInputInfo = ^TGetSomeUserInputInfo;
  TGetSomeUserInputInfo = record
    FMyModelForm: TMyModelForm;
    FModel: TMyModel;
    // lots of in- and output parameters
    FResult: Boolean;
  end;

{ TMyThread }

function TMyThread.GetSomeUserInput(AMyModelForm: TMyModelForm;
  AModel: TMyModel; (* the same parameters as in TGetSomeUserInputInfo *)): Boolean;
var
  GSUII: TGetSomeUserInputInfo;
begin
  GSUII.FMyModelForm := AMyModelForm;
  GSUII.FModel := AModel;
  // Set the input parameters in GSUII

  FpCallbackParams := @GSUII; // FpCallbackParams is a Pointer field in TMyThread
  Synchronize(DelegateGetSomeUserInput);
  // Read the output parameters from GSUII
  Result := GSUII.FResult;
end;

procedure TMyThread.DelegateGetSomeUserInput;
begin
  with PGetSomeUserInputInfo(FpCallbackParams)^ do
    FResult := FMyModelForm.DoGetSomeUserInput(FModel, (* the params go here *));
end;

{ TMyModelForm }

function TMyModelForm.DoGetSomeUserInput(Sender: TMyModel; (* and here *)): Boolean;
begin
  // Show the dialog
end;

function TMyModelForm.GetSomeUserInput(Sender: TMyModel; (* the params again *)): Boolean;
begin
  // The input can be necessary in different situations - some within a thread, some not.
  if Assigned(FMyThread) then
    Result := FMyThread.GetSomeUserInput(Self, Sender, (* the params *))
  else
    Result := DoGetSomeUserInput(Sender, (* the params *));
end;

你们有没有cmets?

【问题讨论】:

    标签: multithreading delphi callback


    【解决方案1】:

    我认为,只要您的长期转换需要用户交互,您就不会对得到的任何答案感到真正满意。因此,让我们回顾一下:为什么需要通过请求更多信息来中断转换?这些真的是您在开始转型之前无法预料的问题吗?当然,用户对中断也不太满意,对吧?他们不能只是开始转型,然后去喝杯咖啡;他们需要坐下来观看进度条,以防出现问题。呃。

    也许转型遇到的问题是可以“保存”到最后的事情。转换是否需要立即知道答案,或者它是否可以完成其他所有事情,然后再做一些“修复”?

    【讨论】:

    • 这就是我想听到的和我害怕同时听到的。 :-) 我同意过多的问题会惹恼用户。但我无权决定。对于大多数问题,我不知道如何将它们移动到转换的开始或结束。
    【解决方案2】:

    绝对选择线程选项(甚至在您编辑后,说您觉得它很复杂)。 The solution that duffymo suggests 在我看来是非常糟糕的 UI 设计(尽管它没有明确地与外观有关,但它与用户与应用程序的交互方式有关)。这样做的程序很烦人,因为您不知道任务将花费多长时间,何时完成等。可以使这种方法变得更好的唯一方法是用生成日期/时间标记结果,但即使那么你需要用户记住他们是什么时候开始这个过程的。

    花时间/精力,让应用程序对您的最终用户有用、信息丰富且不那么令人沮丧。

    【讨论】:

      【解决方案3】:

      为了获得最佳解决方案,无论如何您都必须分析您的代码,并找到所有地方来检查用户是否想要取消长时间运行的操作。对于简单的过程和线程解决方案都是如此 - 您希望操作在十分之几秒后完成,以使您的程序显示为对用户做出响应。

      现在我首先要做的是创建一个接口(或抽象基类),其方法如下:

      IModelTransformationGUIAdapter = interface
        function isCanceled: boolean;
        procedure setProgress(AStep: integer; AProgress, AProgressMax: integer);
        procedure getUserInput1(...);
        ....
      end;
      

      并将过程更改为具有此接口或类的参数:

      procedure MyTransformation(AGuiAdapter: IModelTransformationGUIAdapter);
      

      现在您已准备好在后台线程或直接在主 GUI 线程中实现,一旦您添加了更新进度和检查取消请求的代码,转换代码本身就不需要更改。您只能以不同的方式实现接口。

      我肯定会不使用工作线程,尤其是如果您无论如何都想禁用 GUI。要利用多个处理器内核,您始终可以找到相对分离的转换过程部分,并在它们自己的工作线程中处理它们。这将为您提供比单个工作线程更好的吞吐量,并且使用AsyncCalls 很容易实现。只要你有处理器内核就可以并行启动它们。

      编辑:

      Rob Kennedy 的 IMO this answer 是迄今为止最具洞察力的,因为它不关注实施的细节,而是关注用户的最佳体验。这肯定是你的程序应该优化的地方。

      如果真的没有办法在转换开始之前获取所有信息,或者稍后运行它并修补一些东西,那么你仍然有机会让计算机做更多的工作,以便用户有一个更好的体验。我从您的各种 cmets 中看到,转换过程有很多点,其中执行分支取决于用户输入。想到的一个例子是用户必须在两个备选方案(如水平或垂直方向)之间进行选择 - 您可以简单地使用 AsyncCalls 来启动这两个转换,并且用户选择他的备选方案的那一刻有可能结果已经计算出来,因此您可以简单地显示下一个输入对话框。这将更好地利用多核机器。也许是一个跟进的想法。

      【讨论】:

      • 转换本身不容易并行化。这确实是一系列相互依赖的步骤。我们使用线程作为保持应用程序响应的工具。愚蠢的想法?
      • 好吧,通过概述的设计,您可以在它们之间切换并自行决定。但正如我所说,如果您此时唯一的 GUI 是模态进度指示器,我不会为此使用线程。如果其他工作可以并行完成,多线程效果最好,否则有更容易(调试)的方法。
      • IIUYC GuiAdapter 方法类似于我们已有的方法。但是我们没有使用集中式界面,而是一堆事件。例如。我原始帖子中的 TMyModelForm.GetSomeUserInput 与模型中的一个事件属性挂钩,该属性在转换期间被调用。
      • 确实很相似,我只是更喜欢界面,因为它更直接,更容易设置,更容易测试。你看到在你的 sn-p 中你有更多的间接性,你需要编写更多的胶水代码。如果可以避免,我也不会使用 Synchronize(),请参阅我的 cmets to bott。
      • 当然不是,正如我已经说过的,这是最有见地的答案!希望您能找到解决问题的好方法。
      【解决方案4】:

      TThread 完美且易于使用。

      开发和调试你的慢函数。

      如果准备好了,把调用放到 tthread 执行方法中。 使用 onThreadTerminate 事件找出函数的结束。

      对于用户反馈,请使用同步!

      【讨论】:

      • 这大致就是我们现在正在做的,但由于可能的用户交互量,同步变得非常烦人。 :-)
      • 但是会发生什么样的交互呢?通常有一个进度条和一个取消按钮。
      • 这取决于 - 有时真的只是一个进度条。在其他时候有很多问题:“你想要这里还是那里?” “你想要一个X还是一个Y?”或者:“注意 - 您之前指定的选项 ABC 会随着您最近的更改而崩溃。应该是哪个?”
      • 我喜欢 Delphi 线程,但说“TThread 完美”太夸张了。
      • Uli,那么您应该考虑重新设计该任务。让线程一直运行,直到出现第一个问题。返回 main-vcl 线程,做表单的东西,然后重新启动线程或创建一个新线程。
      【解决方案5】:

      我认为你的愚蠢是将转型视为一项单一任务。如果需要用户输入作为计算的一部分,并且要求的输入取决于当时的计算,那么我会将单个任务重构为多个任务。

      然后您可以运行任务、请求用户输入、运行下一个任务、请求更多输入、运行下一个任务等。

      如果您将流程建模为工作流,则应该清楚需要哪些任务、决策和用户输入。

      我会在后台线程中运行每个任务以保持用户界面的交互性,但不会出现所有编组问题。

      【讨论】:

        【解决方案6】:

        通过向队列发送消息并让侦听器进行处理来进行异步处理。控制器向用户发送一条 ACK 消息,上面写着“我们已收到您的处理请求。请稍后再回来查看结果。”给用户一个邮箱或链接,以便查看进展情况。

        【讨论】:

          【解决方案7】:

          虽然我不完全理解您想要做什么,但我可以提供的是我对可能解决方案的看法。我的理解是,您有一系列 n 事情要做,并且在此过程中对一件事情的决定可能会导致一个或多个不同的事情被添加到“转换”中。如果是这种情况,那么我会尝试(尽可能地)将 GUI 和决策与需要完成的实际工作分开。当用户启动“转换”时,我会(还没有在线程中)循环遍历每个必要的决策,但不执行任何工作......只是询问完成工作所需的问题,然后将步骤与参数到一个列表中。

          当最后一个问题完成后,生成你的线程,将要运行的步骤列表与参数一起传递给它。这种方法的优点是您可以显示 n 个项目中的 1 个的进度条,让用户了解他们在喝完咖啡后回来可能需要多长时间。

          【讨论】:

            【解决方案8】:

            我当然会选择线程。弄清楚线程如何与用户交互通常很困难,但对我来说效果很好的解决方案是不让线程与用户交互,而是让用户端 GUI 与线程交互。这解决了使用同步更新 GUI 的问题,并为用户提供了更多响应活动。

            因此,为此,我使用线程中的各种变量(由使用临界区的 Get/Set 例程访问)来包含状态信息。对于初学者,我有一个 GUI 的“已取消”属性来设置请求线程停止。然后是一个“状态”属性,指示线程是在等待、忙碌还是完成。您可能有一个“人类可读”状态来指示正在发生的事情,或者完成百分比。

            要阅读所有这些信息,只需在表单上使用计时器并进行更新。我也倾向于有一个“statusChanged”属性,如果其他项目之一需要刷新,则设置该属性,这会阻止过多的阅读。

            这对我来说在各种应用中都很有效,包括一个在带有进度条的列表框中显示多达 8 个线程的状态。

            【讨论】:

              【解决方案9】:

              如果您决定使用 Threads,我也发现它们在 Delphi 中的实现方式有些复杂,我会推荐 Primož Gabrijelčič 的 OmniThreadLibraryGabr,因为他在 Stack Overflow 上广为人知。

              这是我所知道的最简单的使用线程库。 Gabr 写了很棒的东西。

              【讨论】:

                【解决方案10】:

                如果您可以将转换代码分成小块,那么您可以在处理器空闲时运行该代码。只需创建一个事件处理程序,将其连接到 Application.OnIdle 事件。只要您确保每个代码块都相当短(您希望应用程序无响应的时间量......比如说 1/2 秒。重要的是在最后将 done 标志设置为 false你的处理程序:

                procedure TMyForm .IdleEventHandler(Sender: TObject;
                  var Done: Boolean);
                begin
                  {Do a small bit of work here}
                  Done := false;
                end;
                

                因此,例如,如果您有一个循环,而不是使用 for 循环,而是使用 while 循环,请确保循环变量的范围在表单级别。在设置 onIdle 事件之前将其设置为零,然后例如每次 onidle 命中执行 10 次循环,直到你到达循环的末尾。

                Count := 0;
                Application.OnIdle := IdleEventHandler;
                
                ...
                ...
                
                procedure TMyForm .IdleEventHandler(Sender: TObject;
                  var Done: Boolean);
                var
                  LocalCount : Integer;
                begin
                  LocalCount := 0;
                
                  while (Count < MaxCount) and (Count < 10) do
                  begin
                    {Do a small bit of work here}
                    Inc(Count);
                    Inc(LocalCount);
                  end;
                  Done := false;
                end;
                

                【讨论】:

                  猜你喜欢
                  • 2010-09-14
                  • 1970-01-01
                  • 2015-07-02
                  • 1970-01-01
                  • 1970-01-01
                  • 2018-06-03
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多