【问题标题】:Not sure how to solve this conflict不知道如何解决这个冲突
【发布时间】:2015-02-07 00:29:44
【问题描述】:

我的 Delphi 应用程序有 2 个活动(似乎)都必须在 UI 线程中发生。在大多数方面,它是一个单线程应用程序。我在这里遇到了部分问题,因为我使用了 Application.ProcessMessages,并且我确实希望尽量减少它,因为它可能会导致问题。

我会先描述成分,然后描述问题。

该应用程序可以打开多个“文档”表单,但为简单起见,我将仅引用一个文档表单。每个表单都可以与外部设备通信。

在某些情况下,程序被构建为循环,直到设备的某种情况允许我们退出循环用户取消操作。设备接口没有线程,必须轮询。

问题来了:

当用户想要关闭文档时,我们可能处于其中一个循环中。这是一个问题,因为所有这些都在 UI 线程中......我没有一个好的方法来打破文档表单中的循环,以便我可以关闭文档。

虽然没有(DeviceReady 或 CancelKeyPressed)做 Application.ProcessMessages;

原解决方案:

我最初的解决方案是在用户尝试关闭文档时执行以下操作:

  1. 向文档表单发布一条用户 Windows 消息,告诉它我们要关闭。
  2. 文档表单接收到消息并中断任何活动循环并将消息发送回主表单,说明我们已准备好关闭。
  3. 主窗体循环、处理消息并轮询检查文档是否已准备好关闭。
  4. 当文档准备关闭时,它将完成文档的关闭。

这在大多数情况下都有效,但它很复杂——尤其是因为我们同时打开了多个文档。

其他可能的解决方案

  • 因为在我看来,所有这些都必须发生在主线程中,将其移至线程听起来不是一个解决方案。

  • 曾想过让文档异步关闭,但我们需要知道文档关闭何时完成,以便让用户执行其他操作,例如打开另一个文档。

  • 循环非常快; 1)发送“停止”消息 2)等待___毫秒并处理消息以使其工作 3)关闭它。我认为这将是一个低质量的设计,会很弱。

  • 将此循环转换为状态机。 (旧版)设备接口没有线程,必须轮询。我需要创建一个线程来进行轮询并将消息发布到表单以引发事件。然后,如果发生此类事件或按下某个键,则会发生一个事件,该事件将导致下一个状态发生。这样,如果要关闭表单,就不会成为问题。遗憾的是,需要努力将周围的代码也转换为参与状态机。 (这还不错。)

结论

Application.ProcessMessages 似乎是一种药物...开始使用它,很快您就需要更多地使用它!

任何人有任何建议如何更好地处理此类问题?

回答您的问题:

(我发布这个问题是为了获得一些关于我应该采取哪种方式的意见;我是一名单独工作的开发人员,因此向社区寻求一些明智的建议是有意义的......)

感谢您的 cmets!

如何管理/引用这些“文档”?

文档表单是对象列表拥有的标准 delphi TForm 后代。

多线程的规则是,如果你需要一个连续的循环,可能导致主线程没有响应,它应该是线程的形式。为什么不允许你把它放在其他地方?为什么一定要在主线程中?

问题是整个应用程序需要等到操作完成或被用户取消。我想我可以将所有传入的键传递给线程。当线程看到循环退出条件满足时,它可以通知 UI 线程。

我喜欢状态机方法。

我也是...它具有一定的优雅和简洁。它还可以简化一些其他相关系统。

为什么不能从线程中与“设备”对话?我会从 UI 中打破那部分。

也许这就是我应该做的。我需要的唯一调用是线程安全的。

很多时候人们可能会告诉您,为了提高性能而将内容移动到线程中是一个常见的错误。但这是你为什么应该这样做的一个主要例子。我无法想象任何严格需要主 UI 线程的界面。您是否在表单设计器中使用组件?如果这是您的挫折,您是否尝试过在不同的线程中动态创建这些组件?

组件包括一个功能区,是的,所有组件都是从 UI 线程创建的。假设我无法在另一个线程中安全地创建组件并将它们放在用户将与之交互的表单上。这肯定不能正常工作吗?

我有足够的线程经验,我实际上可以做到这一点,但我想更确定我没有遗漏什么明显的东西。

【问题讨论】:

  • 这很难理解,因为我看不到你的代码。您目前如何执行此“循环”?这些“文档”是如何管理/引用的?多线程的规则是,如果你需要一个连续的循环,可能导致主线程没有响应,它应该是线程的形式。为什么不允许你把它放在其他地方?为什么一定要在主线程中?
  • 我喜欢状态机方法。
  • 为什么不能从线程中与“设备”对话?我会从 UI 中打破这部分。
  • 很多时候人们可能会告诉你,为了提高性能而将内容移动到线程中是一个常见的错误。但这是你为什么应该这样做的一个主要例子。我无法想象任何严格需要主 UI 线程的界面。您是否在表单设计器中使用组件?如果这是您的挫折,您是否尝试过在不同的线程中动态创建这些组件?
  • 感谢大家的cmets!我采取了不同寻常的步骤,在问题本身的末尾回复他们。我认为共识是最好使用线程或状态机来完成。我需要深入研究并做出选择。谢谢大家!

标签: delphi user-interface


【解决方案1】:

思路很简单:

为文档构建一个基类并将基函数(在后台处理一些操作)放入其中

unit BaseDocument;

interface

uses
  System.Classes,
  System.SysUtils,
  System.Threading;

type
  TBaseDocument = class
  private
    FIsReady: Boolean;
    FIsReadyChanged: TNotifyEvent;
    procedure SetIsReady( const Value: Boolean );
    procedure SetIsReadyChanged( const Value: TNotifyEvent );
  protected
    procedure PerformBackgroundAction( AProc: TProc );
  public
    property IsReady: Boolean read FIsReady;
    property IsReadyChanged: TNotifyEvent read FIsReadyChanged write SetIsReadyChanged;
  end;

implementation

{ TBaseDocument }

procedure TBaseDocument.PerformBackgroundAction( AProc: TProc );
begin
  SetIsReady( False );
  TTask.Run( nil,
      procedure
    begin
      try
        AProc( );
      finally
        TThread.Synchronize( nil,
            procedure
          begin
            SetIsReady( True );
          end );
      end;
    end );
end;

procedure TBaseDocument.SetIsReady( const Value: Boolean );
begin
  if FIsReady <> Value
  then
    begin
      FIsReady := Value;
      if Assigned( FIsReadyChanged )
      then
        FIsReadyChanged( Self );
    end;
end;

procedure TBaseDocument.SetIsReadyChanged( const Value: TNotifyEvent );
begin
  FIsReadyChanged := Value;
end;

end.

如您所见,我们有一个 IsReady 作为 UI 部分的指示器,用于指示文档是否已准备就绪,并且在 IsReady 更改其状态时触发一个事件。

现在我们需要一个基础文档表单来处理这个问题。这也很容易,因为我们只想让用户远离点击控件。我们只是在所有表单内容上放了一个东西(这里是 ActivityLayout : TLayoutTLayout.Hittest 设置为 True)。

ActivityLayout 包含一个黑色矩形,不透明度设置为 30% 以使表单内容变暗,TAiniIndicator 向用户提供一些信息,表明仍有一些操作正在进行中。

一旦文档更改IsReady 状态,表单就会收到通知并显示或隐藏ActivityLayout

unit Form.BaseDocument;

interface

uses
  BaseDocument,

  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.StdCtrls,
  FMX.Objects, FMX.Layouts;

type
  TBaseDocumentForm = class( TForm )
    ActivityLayout: TLayout; { container for ActivityIndicators }
    ActivityCurtain: TRectangle; { darken the form content }
    ActivityIndicator: TAniIndicator; { shows an animation while activity is in progress }
    procedure FormCloseQuery( Sender: TObject; var CanClose: Boolean );
  private
    FDocument: TBaseDocument;
    function CanFormClose: Boolean;
  protected
    procedure SetDocument( ADocument: TBaseDocument );
    procedure DoShowActivity( const AVisible: Boolean );
    procedure DocumentIsReadyChanged( Sender: TObject );
    procedure DoReloadDocument; virtual;
  public

  end;

var
  BaseDocumentForm: TBaseDocumentForm;

implementation

{$R *.fmx}
{ TBaseDocumentForm }

function TBaseDocumentForm.CanFormClose: Boolean;
begin
  Result := not Assigned( FDocument ) or Assigned( FDocument ) and FDocument.IsReady;
end;

procedure TBaseDocumentForm.DocumentIsReadyChanged( Sender: TObject );
begin
  DoShowActivity( not FDocument.IsReady );
  if FDocument.IsReady
  then
    DoReloadDocument;
end;

procedure TBaseDocumentForm.DoReloadDocument;
begin
  // override this to load the document after being ready
end;

procedure TBaseDocumentForm.FormCloseQuery( Sender: TObject; var CanClose: Boolean );
begin
  CanClose := CanFormClose;
end;

procedure TBaseDocumentForm.SetDocument( ADocument: TBaseDocument );
begin
  if FDocument <> ADocument
  then
    begin
      if Assigned( FDocument ) and ( FDocument.IsReadyChanged = DocumentIsReadyChanged )
      then
        FDocument.IsReadyChanged := nil;

      FDocument := ADocument;

      if Assigned( FDocument )
      then
        begin
          FDocument.IsReadyChanged := DocumentIsReadyChanged;
          DocumentIsReadyChanged( FDocument );
        end
      else
        DoShowActivity( False );
    end;
end;

procedure TBaseDocumentForm.DoShowActivity( const AVisible: Boolean );
begin
  ActivityLayout.Visible := AVisible;
  if AVisible
  then
    begin
      { just to ensure the right order }
      ActivityLayout.BringToFront;
      ActivityCurtain.BringToFront;
      ActivityIndicator.BringToFront;
    end;
end;

end.

这可以很容易地扩展到使用可中断的后台操作和ActivityLayout 上的取消按钮。


附言

线程安全只需要,如果多个线程正在执行,同时访问同一个。如果你能确保它在给定的时间跨度内只能被一个线程访问,那么就不需要让它成为线程安全的。

【讨论】:

  • 感谢您提供有趣的解决方案;这无疑拓宽了我对这个问题的思考方式。我的应用程序是一个 VCL 应用程序,但这并没有太大改变您的建议的用处。
【解决方案2】:

您说您的循环非常快,但似乎还不够快。现在我假设你已经尝试让它们更快,所以我不会建议这个。

但是您可能忘记了一个事实,即可以使用 Break 命令在循环的中间中断循环。因此,也许您应该检查循环周期内是否有可能检查 CancelKeyPressed 的值的位置,然后调用 Break 命令,您将使用该命令提前结束循环。

while not (DeviceReady or CancelKeyPressed) do 
begin
  //Do some work
  ...
  //Check if premature exit condition is set
  if CancelKeyPressed then
  begin
    //Do some clean up if needed
    ...
    Break;
  end;
  //Do some more work
  ...
  //Check again if premature exit condition is set
  if CancelKeyPressed then
  begin
    //Do some clean up if needed
    ...
    Break;
  end;
  Application.ProcessMessages;
end;

现在您在使用 Break 命令时应特别注意,尤其是当您在循环内动态创建和释放某些对象时。为什么?

因为将 break 命令放在错误的位置可能会阻止其中一些对象被正确释放,从而导致内存泄漏。

在这种情况下,您需要确保在释放所有这些对象之后调用 Break 命令之前。因此,要么确保将 break 放在执行释放的现有代码之后,要么添加仅在设置循环过早退出条件时才执行释放的附加代码。

说到控制循环,您可能需要检查 Continue 命令,该命令允许您中断当前循环循环并继续下一个循环。

现在这个命令不常用了,因为你可以通过简单地将循环代码的各个块放在 if stamtents 中来实现类似的事情。因此,如果满足某个条件,则执行代码,否则它不会执行,您可能已经处于循环周期的末尾。

无论如何,我建议您检查提前结束循环的能力的主要原因是,即使您确实实现了多线程,您仍然需要等待循环结束。尤其是在每个循环周期结束时,您在 Syncronize 命令的帮助下更新了一些 UI 控件。

您不希望销毁表单,然后让您的其他线程尝试更新现在不存在的 UI 组件,对吗?我猜这不会导致一堆访问冲突。

所以我建议您首先检查是否有办法提前结束循环。然后我还建议您尝试将该代码移动到单独的线程中。

主要原因是您经常使用 Application.ProcessMessages 可能是降低循环性能的主要原因,因为在 Application.ProcessMessages 完成工作之前循环不会继续。

【讨论】:

  • 谢谢;我实际上非常熟悉 Break 关键字并经常使用它。我知道使用 Application.ProcessMessages 会降低性能。在最初构建产品时,它是完成工作的解决方案。您的解决方案无法满足我的需求。
  • 我从没想过仅此一项就能解决您的问题。因为我不认为将循环代码移动到单独的线程中,除非 Application.ProcessMessages 调用,你可以通过正确实现多线程来避免是减速的主要原因。但即使使用多线程,你可能仍然需要等待你的在你去破坏表格之前循环循环结束。如果这需要很长时间,您仍然会面临与现在类似的问题。所以正如我所写的,我的建议是尝试将它与多线程结合使用。
猜你喜欢
  • 1970-01-01
  • 2012-02-18
  • 2015-01-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-26
  • 1970-01-01
相关资源
最近更新 更多