如何从主UI线程中处理异步队列?

时间:2016-07-19 10:48:08

标签: delphi firemonkey

我正在设计两个异步接收自定义类(TMELogMessage)对象的组件,并将它们存储在一个线程安全的内部容器中。第一个组件是非可视化的(TMEFileLogger),应该将这些对象的一些信息写入日志文件(非常令人惊讶)。第二个组件(TMELogGrid)是一个可视FMX.Grid后代,它应该可视化UI中这些对象的一些信息。但是,我认为他们对这些物品所做的事情是无关紧要的。

我面临的问题是这些组件实际上并不知道这些对象何时会在其内部容器中排队,因此他们必须自己检查容器以查看是否有任何需要处理的新对象,处理它们以及从队列中删除它们。理想情况下,我希望在应用程序不太繁忙时以与动作更新类似的方式完成此操作,以免陷入UI。

组件挂钩像Application.OnIdle这样的事件处理程序显然是错误的...我可以订阅TIdleMessage,但我不确定这是个好主意,因为我读过一些应用程序永远不会闲置。使用内部计时器似乎有点老了。我可以使用低优先级线程来轮询队列,然后仅在我找到要处理的对象时才与UI同步。我虽然没有其他想法。

在Delphi + multiplatform FireMonkey中执行此操作的正确方法是什么?

2 个答案:

答案 0 :(得分:1)

队列实现通常实现应用程序代码可以等待的事件(OS synchronization object,而不是VCL'事件')。事件被设置/触发/触发/但是每当项目被添加到空队列时(或者,如果在“批处理”中添加多个项目,则在添加完所有项目之后)您想要考虑它。精确的模式可能变化)。如果您的案例中的队列是您自己的实现,那么我会考虑在您的实现中添加这样的机制。

为了避免阻止UI,应用程序代码创建一个轻量级线程,其唯一目的是等待该队列事件,将队列中的项目排队到UI线程安全容器中,然后通知UI线程有项目待处理。监控线程然后继续等待事件发出信号表明队列中还有更多项。

在VCL应用程序中,监视线程通知UI的机制可能是一个简单的Synchronize d过程或(我建议)基于消息的通知发布到负责项目的UI处理的某个表单

注意:队列监控线程通常还负责处理应用程序/ UI不再关心处理项目(例如正在关闭)的情况,因此也会监听“取消”或“终止”事件,该信号指示线程对项目进行排队,但丢弃它们(或以适合此应用程序需要的方式处理它们)然后终止(即,监视线程退出)。

答案 1 :(得分:1)

我不想回答我自己的问题,但我希望回答这个问题,因为这可能对其他人有所帮助。虽然Deltics的回答很有用,但这不是我决定采用的方式。我遵循了Remy评论中的建议,并将所有内容封装在组件和表单可以使用的消息处理类中。因此,TMEFileLogger和TMELogGrid现在都使用这个新的TMEMessageHandler类的实例。

这是一些界面代码来解释我的所作所为。请记住,这是rtl System.Messaging单元的替代和增强。 rtl消息传递系统的问题在于它仅提供发送同步消息。我想要一个更丰富的界面。这就是我的消息管理器的样子:

  TMEMessageManager = Class
    ...
  Public
    ...
    Procedure PostDelayedEnvelope(Const Envelope: TMEMessageEnvelope; Const DelayMSec: Cardinal; Const ADispose: Boolean = True); Inline;
    Procedure PostDelayedMessage(Const Sender: TObject; AMessage: TMessage; Const DelayMSec: Cardinal; Const ADispose: Boolean = True); Inline;
    Procedure PostEnvelope(Const Envelope: TMEMessageEnvelope; Const ADispose: Boolean = True); Inline;
    Procedure PostMessage(Const Sender: TObject; AMessage: TMessage; Const ADispose: Boolean = True); Inline;
    Procedure SendEnvelope(Const Envelope: TMEMessageEnvelope; Const ADispose: Boolean = True); Inline;
    Procedure SendMessage(Const Sender: TObject; AMessage: TMessage; Const ADispose: Boolean = True); Inline;

    Function Subscribe(Const AMessageClass: TClass; Const AReceiver: IMEEnvelopeReceiver): Integer; Overload;
    Function Subscribe(Const AMessageClass: TClass; Const AMethod: TMessageMethod): Integer; Overload; Deprecated 'Use TMEMessageManager.Subscribe(AMessageClass, AReceiver)';
    Function Subscribe(Const AMessageClass: TClass; Const AProcedure: TMessageProcedure): Integer; Overload; Deprecated 'Use TMEMessageManager.Subscribe(AMessageClass, AReceiver)';

    Procedure Unsubscribe(Const AMessageClass: TClass; ID: Integer; Const Immediate: Boolean = False); Overload;
    Procedure Unsubscribe(Const AMessageClass: TClass; Const AReceiver: IMEEnvelopeReceiver; Const Immediate: Boolean = False); Overload;
    Procedure Unsubscribe(Const AMessageClass: TClass; Const AMethod: TMessageMethod; Const Immediate: Boolean = False); Overload; Deprecated;
    Procedure Unsubscribe(Const AMessageClass: TClass; Const AProcedure: TMessageProcedure; Const Immediate: Boolean = False); Overload; Deprecated;
    ...
  End;

TMEMessageEnvelope是消息的包装器,定义为:

Type
  TMEMessageEnvelope = Class(TMEPersistent)
  Public
    ...
    Property Infos: TMEMessageInfos Read FInfos;
    Property Sender: TObject Read FSender;
    Property Msg: TMessage Read FMsg;
  End;

通过信封接收器订阅的接收器将同时接收同步和异步消息。这是首选的订阅方法。通过对象方法或通过过程订阅的接收器将仅接收同步消息。这是为了与RTL消息传递系统兼容而维护,但已弃用。

问题是RTL消息无法发布,因为它们是。订阅的消费者只是立即提供消息消息的过程或对象方法。要发布消息以便以后可以异步使用它,它需要被包装和排队。这样发送方与接收方隔离。实际上......通过首先将信息包装在信封中来发布(立即或延迟)消息。

以下是此邮件系统中包含的基本接口:

Type

  IMEClonableMessage = Interface(IInterface)
    ['{45B223E2-DCA8-4E42-9847-6A3FCC910891}']
    Function Clone: TMessage;
  End;

  IMEMessageSender = Interface(IInterface)
    ['{99AFDC4A-CE30-41A3-9AA5-D49F2F1106BD}']
    Procedure PostDelayedMessage(const M: TMessage; Const DelayMSec: Cardinal);
    Procedure PostMessage(Const M: TMessage);
    Procedure SendMessage(Const M: TMessage);
  End;

  IMEEnvelopeSender = Interface(IInterface)
    ['{C3AEC52C-A558-40AB-B07B-3000ECDB9C0C}']
    Procedure PostDelayedEnvelope(Const Envelope: TMEMessageEnvelope; Const DelayMSec: Cardinal);
    Procedure PostEnvelope(Const Envelope: TMEMessageEnvelope);
    Procedure SendEnvelope(Const Envelope: TMEMessageEnvelope);
  End;

  IMEEnvelopeReceiver = Interface(IInterface)
    ['{7D464713-2F25-4666-AAF8-757AF07688C3}']
    Procedure ClearEnvelopes;
    Procedure ProcessEnvelope;
    Procedure ProcessEnvelopes;
    Function QueueEnvelope(Const Envelope: TMEMessageEnvelope): Integer;
    Procedure ReceiveEnvelope(Const Envelope: TMEMessageEnvelope);
    Procedure Subscribe(Const AMessageClass: TClass);
    Procedure Unsubscribe(Const AMessageClass: TClass);
  End;

IMEClonableMessage接口用于克隆消息...必须克隆异步消息...因为如果同一消息有许多订阅者,则每个消息都会在不同时间接收和使用消息,因此最好每个有自己的信息副本。

我认为其他界面是不言自明的。

最后这里是TMEMessageHandler类:

  TMEMessageHandler = Class(TMEPersistent, IMEMessageSender, IMEEnvelopeSender, IMEEnvelopeReceiver)
    /// <summary>Basic thread-safe class that can send and receive synchronous and asynchronous messages and envelopes.</summary>
  Private
    FLock:                 TObject;
    FMessageManager:       TMEMessageManager;
    FSubscriptions:        TDictionary<TClass, Integer>;
    FEnvelopes:            TObjectList<TMEMessageEnvelope>;
    FOnReceiveEnvelope:    TReceiveEnvelopeEvent;
    FAutoProcessEnvelopes: Boolean;
    Procedure _Lock;
    Procedure _Unlock;
    Procedure ClearSubscriptions;
    Function GetMessageManager: TMEMessageManager;
    Procedure SetAutoProcessEnvelopes(Const Value: Boolean);
    Procedure SetMessageManager(Const Value: TMEMessageManager);
  Protected
    Function QueryInterface(Const IID: TGuid; Out Obj): HResult; Stdcall;
    Function _AddRef: Integer; Stdcall;
    Function _Release: Integer; Stdcall;
    Procedure DoReceiveEnvelope(Const Envelope: TMEMessageEnvelope);
    Procedure PostDelayedEnvelope(Const Envelope: TMEMessageEnvelope; Const DelayMSec: Cardinal);
    Procedure PostDelayedMessage(Const M: TMessage; Const DelayMSec: Cardinal);
    Procedure PostEnvelope(Const Envelope: TMEMessageEnvelope);
    Procedure PostMessage(Const M: TMessage);
    Procedure SendEnvelope(Const Envelope: TMEMessageEnvelope);
    Procedure SendMessage(Const M: TMessage);
    Function QueueEnvelope(Const Envelope: TMEMessageEnvelope): Integer;
    Procedure ReceiveEnvelope(Const Envelope: TMEMessageEnvelope);
  Public
    Constructor Create; Override;
    Destructor Destroy; Override;
    Procedure ClearEnvelopes;
    Procedure ProcessEnvelope;
    Procedure ProcessEnvelopes;
    Procedure Subscribe(Const AMessageClass: TClass);
    Procedure Unsubscribe(Const AMessageClass: TClass);
    Property AutoProcessEnvelopes: Boolean Read FAutoProcessEnvelopes Write SetAutoProcessEnvelopes Default True;
    Property MessageManager: TMEMessageManager Read GetMessageManager Write SetMessageManager;
    Property OnReceiveEnvelope: TReceiveEnvelopeEvent Read FOnReceiveEnvelope Write FOnReceiveEnvelope;
  End;

这一切是如何运作的

TMEMessageHandler立即将任何订阅和取消订阅调用委托给MessageManager。它将始终订阅提供自己作为IMEEnvelopeReceiver。它在内部字典中跟踪订阅,以便在取消订阅时更有效。

它还会立即委托对Send,Post和PostDelayed方法的任何调用。 TMEMessageManager:

  • 向订阅程序发送消息(如RTL)
  • 将消息发送到订阅对象方法(如RTL)
  • 通过拨打他们的信息,将信封发送给订阅的接收者 ReceiveEnvelope方法
  • 将信封(和信封包装的信息)张贴到订阅 接收器通过调用其克隆副本的QeueEnvelope方法 信封
  • 发布延迟信封(和信封包装的信息)订阅 接收器首先将它们放入内部轻量级线程中 (TMEDelayedEnvelopeDeliverer)具有消息管理器本身 在延迟过去后交付

作为接收者,TMEMessageHandler通过简单地委托给OnReceiveEnvelope事件处理程序来实现ReceiveEnvelope。

QueueEnvelope方法接收已发布的信封,该方法在其线程安全队列中添加信封,然后,但仅当AutoProcessEnvelopes为True时,才使用主线程的Queue调用其自己的ProcessEnvelope方法(如Remy的建议):

Function TMEMessageHandler.QueueEnvelope(Const Envelope: TMEMessageEnvelope): Integer;
Begin
  _Lock;
  Try
    FEnvelopes.Add(Envelope);
    Result := FEnvelopes.Count;
  Finally
    _Unlock;
  End;
  If AutoProcessEnvelopes Then
    TThread.Queue(Nil,
      Procedure
      Begin
        ProcessEnvelope;
      End);
End;

ProcessEnvelope方法从线程安全队列中提取信封,调用ReceiveEnvelope方法(与消息管理器为同步消息调用的方法相同),然后释放信封(请记住,这是一个仅用于此接收器的克隆副本) :

Procedure TMEMessageHandler.ProcessEnvelope;
Var
  E: TMEMessageEnvelope;
Begin
  If FEnvelopes.Count > 0 Then Begin
    _Lock;
    Try
      E := FEnvelopes.Extract(FEnvelopes[0]);
    Finally
      _Unlock;
    End;
    E.UpdateInfo(mieReceived);
    ReceiveEnvelope(E);
    E.Free;
  End;
End;

ProcessEnvelopes方法只需要多次调用前者来清空异步消息队列:

Procedure TMEMessageHandler.ProcessEnvelopes;
Begin
  While FEnvelopes.Count > 0 Do
    ProcessEnvelope;
End;

TMEMessageHandler如何使用

将TMELogMessage定义为IMEClonableMessage以处理要记录的信息,TMEFileLogger和其他组件的最小实现如下所示:

Type
  TMEFileLogger = Class(TMEComponent)
  Private
    ...
    FMessageHandler:    TMEMessagehandler;
  Protected
    ...
    Procedure ReceiveLogEnvelope(Const Envelope: TMEMessageEnvelope);
    Property MessageHandler: TMEMessagehandler Read FMessageHandler;
  Public
    Constructor Create(AOwner: TComponent); Override;
    Destructor Destroy; Override;
    ...
  End;

Constructor TMEFileLogger.Create(AOwner: TComponent);
Begin
  Inherited;
  ...
  FMessageHandler                  := TMEMessagehandler.Create;
  MessageHandler.OnReceiveEnvelope := ReceiveLogEnvelope;
  MessageHandler.Subscribe(TMELogMessage);
End;

Destructor TMEFileLogger.Destroy;
Begin
  MessageHandler.Unsubscribe(TMELogMessage);
  MessageHandler.ProcessEnvelopes;
  FreeAndNil(FMessageHandler);
  ...
  Inherited;
End;

Procedure TMEFileLogger.ReceiveLogEnvelope(Const Envelope: TMEMessageEnvelope);
Begin
  If Envelope.Msg Is TMELogMessage Then
    With Envelope.Msg As TMELogMessage Do
      ... something useful ...
End;

对于这篇长篇文章感到抱歉,但我希望这对其他人有用。