在长期任务中保持应用响应

时间:2009-01-12 18:45:01

标签: multithreading delphi callback

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

可能的方法:

  1. 忽略这个问题,只需将转换代码放入一个过程并调用它。不好,因为在转换需要一些时间但不需要用户输入的情况下应用程序似乎挂起。
  2. 使用回调来填充代码:这很突兀 - 您必须在转换代码中添加很多这些调用 - 并且不可预测 - 您永远无法确定是否找到了合适的地方。
  3. 使用Application.ProcessMessages填充代码:与回调相同的问题。此外,您还可以获得ProcessMessages的所有问题。
  4. 使用线程:这使我们免于2.和3的“突兀和不可预测”部分。然而,由于用户输入所需的“编组”,它需要做很多工作 - 调用Synchronize,put any在量身定制的记录等中需要参数。调试也容易出错,这也是一个噩梦。
  5. //编辑:我们当前的解决方案是一个线程。然而,由于用户输入,这是一个痛苦的**。在很多例程中可能会有很多输入代码。这让我感觉线程正确的解决方案。

    我将自己进行骚扰并发布我生成的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;
    

    你有什么意见吗?

10 个答案:

答案 0 :(得分:7)

我认为只要您长时间运行的转换需要用户互动,您就不会对任何答案感到满意。所以让我们回顾一下:为什么你需要通过请求获取更多信息来中断转换?在开始转型之前,这些问题真的是你无法预料到的吗?当然,用户对中断也不太满意,对吧?他们不仅可以设定转型,还可以获得一杯咖啡;如果出现问题,他们需要坐下来观看进度条。啊。

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

答案 1 :(得分:4)

在编辑后肯定选择一个线程选项( 甚至 ,说你发现它很复杂)。在我看来,The solution that duffymo suggests是非常差的UI设计(即使它没有明确地说明外观,它是关于用户如何与您的应用程序连接)。执行此操作的程序很烦人,因为您不知道任务需要多长时间,何时完成等等。这种方法可以做得更好的唯一方法是使用生成日期/时间标记结果,但即使是那么你需要用户记住他们何时开始这个过程。

花费时间/精力,使应用程序变得有用,信息丰富,让您的最终用户不那么沮丧。

答案 2 :(得分: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是最具洞察力的,因为它不关注实施细节,而是关注用户的最佳体验。这肯定是你的程序应该优化的东西。

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

答案 3 :(得分:2)

TThread非常完美且易于使用。

开发和调试慢速函数。

如果准备就绪,将调用放入tthread执行方法。 使用onThreadTerminate事件找出你的功能结束。

用户反馈使用syncronize!

答案 4 :(得分:2)

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

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

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

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

答案 5 :(得分:1)

通过向队列发送消息并让侦听器执行处理来异步处理。控制器向用户发送一条ACK消息,说明“我们已收到您的处理请求。请稍后再查看结果。”为用户提供邮箱或链接以进行检查并查看事情的进展情况。

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

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

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

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

这在我的各种应用程序中运行良好,包括在带有进度条的列表框中显示最多8个线程的状态。

答案 8 :(得分:1)

如果您决定使用Threads,我发现它在Delphi中的实现方式有些复杂,我建议使用PrimožGabrijelčič的OmniThreadLibraryGabr,因为他在Stack Overflow中已知

这是我所知道的最简单的使用线程库。加布尔写了很多东西。

答案 9 :(得分:0)

如果您可以将转换代码拆分为小块,那么您可以在处理器空闲时运行该代码。只需创建一个事件处理程序,将其连接到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;