不确定如何解决此冲突

Not sure how to solve this conflict

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

先说成分再说问题

该应用程序可以打开多个 "document" 表单,但为了简单起见,我将只提及一个文档表单。每个表单都可以与外部设备通信。

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

这是问题所在:

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

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

原解:

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

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

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

其他可能的解决方案

结论

Application.ProcessMessages 看起来像毒品...开始使用它,很快您就需要更多地使用它!

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

您问题的答案:

(我 post 编辑了这个问题是为了获得一些关于我应该采用哪种方式的意见;我是一个独自工作的开发人员,在这种情况下向社区询问一些圣人是有意义的建议...)

感谢您的意见!

How are these "Documents" managed/referenced?

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

The rule of multi-threading is, if you need a continuous loop which could cause the main thread to not respond, it should be in the form of a thread. Why aren't you allowed to put it elsewhere? Why must it be in the main thread?

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

I like the state machine approach.

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

Why can't you talk to the "equipment" from threads? I would break that part from the UI.

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

There are many times people may tell you that it's a common mistake to move things into a thread just to improve performance. But this is one prime example of why you should. I can't imagine any interface which strictly requires the main UI thread. Are you using components dropped-in to the form designer? If that's your setback, have you tried dynamically creating those components in a different thread?

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

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

思路很简单:

为文档构建一个基础 class 并将基础功能(在后台处理一些操作)放在那里

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 上的取消按钮。


PS

Thread Safe只需要,如果多个线程在执行,同时访问同一个。如果您可以确保在给定的时间跨度内它只会被一个线程访问,那么就没有必要让它成为线程安全的。

你说你的循环很快,但好像不够快。现在我假设您已经尝试让它们更快,所以我不建议这样做。

但是您可能忘记了一个事实,即可以使用 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 完成它工作。