不确定如何解决此冲突
Not sure how to solve this conflict
我的 Delphi 应用程序有 2 个活动(表面上)必须都发生在 UI 线程中。在大多数方面,它是一个单线程应用程序。我在这里遇到了部分问题,因为我使用了 Application.ProcessMessages 并且我确实希望将其最小化,因为它可能会导致问题。
先说成分再说问题
该应用程序可以打开多个 "document" 表单,但为了简单起见,我将只提及一个文档表单。每个表单都可以与外部设备通信。
在某些情况下,程序被构建为循环,直到设备的特定情况允许我们退出循环或用户取消操作。设备接口不是线程化的,必须轮询。
这是问题所在:
当用户想要关闭文档时,我们可能处于这些循环之一。这是个问题,因为所有这些都在 UI 线程中...我没有打破文档表单循环的好方法,所以我可以关闭文档。
虽然没有(DeviceReady 或 CancelKeyPressed)做
Application.ProcessMessages;
原解:
我最初的解决方案是在用户试图关闭文档时执行以下操作:
- post 用户 Windows 向文档表单发送消息,告诉它我们要关闭。
- 文档表单收到消息并中断任何活动循环并向主表单发回一条消息,说明我们已准备好关闭。
- 主窗体循环、处理消息并轮询检查文档是否准备好关闭。
- 当文档准备关闭时,它将完成文档的关闭。
这在大多数情况下都有效,但它很复杂——尤其是因为我们同时打开了多个文档。
其他可能的解决方案
因为在我看来所有这些都必须发生在主线程中,所以将其移动到线程听起来不像是解决方案。
曾想过让文档异步关闭,但我们需要知道文档关闭何时完成,这样我们才能让用户做其他事情,比如打开另一个文档。
循环非常快; 1) 发送 "stop" 消息 2) 等待 ___ 毫秒并处理消息以使其工作 3) 关闭它。我认为这将是一个质量较差的低质量设计。
将此循环改为状态机。 (遗留)设备接口不是线程化的,必须轮询。我需要创建一个线程来执行轮询和 post 向表单发送消息以引发事件。然后,如果发生这样的事件或按下某个键,则会发生一个事件,导致下一个状态发生。这样,如果要关闭表单,它永远不会成为问题。遗憾的是,转换周围代码以参与状态机也需要付出努力。 (这还不错。)
结论
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 : TLayout
,TLayout.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 完成它工作。
我的 Delphi 应用程序有 2 个活动(表面上)必须都发生在 UI 线程中。在大多数方面,它是一个单线程应用程序。我在这里遇到了部分问题,因为我使用了 Application.ProcessMessages 并且我确实希望将其最小化,因为它可能会导致问题。
先说成分再说问题
该应用程序可以打开多个 "document" 表单,但为了简单起见,我将只提及一个文档表单。每个表单都可以与外部设备通信。
在某些情况下,程序被构建为循环,直到设备的特定情况允许我们退出循环或用户取消操作。设备接口不是线程化的,必须轮询。
这是问题所在:
当用户想要关闭文档时,我们可能处于这些循环之一。这是个问题,因为所有这些都在 UI 线程中...我没有打破文档表单循环的好方法,所以我可以关闭文档。
虽然没有(DeviceReady 或 CancelKeyPressed)做 Application.ProcessMessages;
原解:
我最初的解决方案是在用户试图关闭文档时执行以下操作:
- post 用户 Windows 向文档表单发送消息,告诉它我们要关闭。
- 文档表单收到消息并中断任何活动循环并向主表单发回一条消息,说明我们已准备好关闭。
- 主窗体循环、处理消息并轮询检查文档是否准备好关闭。
- 当文档准备关闭时,它将完成文档的关闭。
这在大多数情况下都有效,但它很复杂——尤其是因为我们同时打开了多个文档。
其他可能的解决方案
因为在我看来所有这些都必须发生在主线程中,所以将其移动到线程听起来不像是解决方案。
曾想过让文档异步关闭,但我们需要知道文档关闭何时完成,这样我们才能让用户做其他事情,比如打开另一个文档。
循环非常快; 1) 发送 "stop" 消息 2) 等待 ___ 毫秒并处理消息以使其工作 3) 关闭它。我认为这将是一个质量较差的低质量设计。
将此循环改为状态机。 (遗留)设备接口不是线程化的,必须轮询。我需要创建一个线程来执行轮询和 post 向表单发送消息以引发事件。然后,如果发生这样的事件或按下某个键,则会发生一个事件,导致下一个状态发生。这样,如果要关闭表单,它永远不会成为问题。遗憾的是,转换周围代码以参与状态机也需要付出努力。 (这还不错。)
结论
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 : TLayout
,TLayout.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 完成它工作。