如何从主UI线程中处理异步队列? [英] How to process an asynchronous queue from within the main UI thread?

查看:73
本文介绍了如何从主UI线程中处理异步队列?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在设计两个组件,这些组件异步接收自定义类(TMELogMessage)的对象,并将它们存储在线程安全的内部容器中。第一个组件是不可见的(TMEFileLogger),应该将这些对象的一些信息写入日志文件(这并不奇怪)。第二个组件(TMELogGrid)是可视的FMX.Grid后代,应在UI中可视化来自这些对象的某些信息。但是,我认为它们与这些对象的作用无关。

I am designing two components that asynchronously receive objects of a custom class (TMELogMessage) and store them in a thread-safe internal container. The first component is non visual (TMEFileLogger) and should write some info from these objects to a log file (non surprisingly). The second component (TMELogGrid) is a visual FMX.Grid descendant that should visualize some info from these objects in the UI. But what they do with these objects is, I think, irrelevant.

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

The problem I am facing is that these components do not actually know when these objects will be enqueued in their internal container, so they have to check the container themselves to see if there are any new objects that need processing, process them and remove them from the queue. Ideally I'd want this to be done when the application is not too busy, in a way similar to action updating, so as not to bog down the UI.

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

It is obviously wrong for a component to hook an event handler like Application.OnIdle... I could maybe subscribe to the TIdleMessage, but I'm not sure that is a good idea, as I've read that a some applications could never go idle. Using an internal timer seems a bit old-school. I could maybe use a low priority thread to poll the queue and then synchronize with the UI only when I find some object to process. I don't have other ideas though.

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

What is the proper way to do this in Delphi + multiplatform FireMonkey?

推荐答案

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

I don't like to answer my own questions, but I wanted this question to be answered, as it might be helpful to others. While Deltics' answer is useful, that is not the way I decided to go. I followed the advice in Remy's comment and encapsulated everything in a message handling class that components and forms can use. So both the TMEFileLogger and the TMELogGrid now use an instance of this new TMEMessageHandler class.

下面是一些接口代码来说明我的工作。请记住,这将替代rtl System.Messaging单元。 rtl消息传递系统的问题在于它仅提供发送同步消息的功能。我想要一个更丰富的界面。这是我的消息管理器的外观:

Here is some interface code to explain what I did. Keep in mind that this is to be a substitute and enhancement of the rtl System.Messaging unit. The problem with the rtl messaging system is that it provides only for sending synchronous messages. I wanted a richer interface. This is what my message manager looks like:

  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是消息的包装,定义为:

The TMEMessageEnvelope is a wrapper for messages, defined as:

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

通过信封接收器订阅的接收器将接收同步和异步消息。这是首选的订阅方法。通过对象方法或过程进行订阅的接收方将仅接收同步消息。维护该属性是为了与RTL消息传递系统兼容,但不建议使用。

Receivers that subscribe via an envelope receiver will receive both synchronous and asynchronous messages. This is the preferred subscription method. Receivers that subscribe via an object method or via a procedure will only receive synchronous messages. This is maintained for compatibility with the RTL messaging system, but is deprecated.

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

The problem is that RTL messages cannot be posted, as they are. The subscribed consumers just provide a procedure or an object method to consume the message, right away. To post the message so as it can be consumed later, asynchronously, it needs to be wrapped and enqueued. This way the sender is isolated from the receivers. So actually... messages are posted (immediately or delayed) by first wrapping them in envelopes.

以下是此消息传递系统中包含的基本接口:

Here are the base interfaces envolved in this messaging system:

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接口用于克隆消息...必须克隆异步消息...因为如果同一条消息有很多订户,每个订户将在不同的时间接收和使用该消息,因此最好每个订户都有自己的消息副本。

The IMEClonableMessage interface is used to clone messages... asynchronous messages must be cloned... because if there are many subscribers to the same message, each will receive and consume the message in different times, so it is best that each has its own copy of the message.

最后是TMEMessageHandler类:

And finally here is the TMEMessageHandler class:

  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进行订阅。

How all this works

The TMEMessageHandler immediately delegates to the MessageManager any Subscribe and Unsubscribe calls. It will always subscribe providing itself as an IMEEnvelopeReceiver. It keeps track of subscriptions in its internal dictionary so as to be more efficient when it unsubscribes.

它还会立即将所有调用委派给Send,Post和PostDelayed方法。 TMEMessageManager:

It also immediately delegates any call to the Send, Post and PostDelayed methods. The TMEMessageManager:


  • 将消息发送到订阅过程(作为RTL)

  • 将消息发送到订阅对象方法(作为RTL)

  • 通过调用信封的
    ReceiveEnvelope方法将信封发送给订阅的收件人

  • 将信封(和信封包装的消息)邮寄到通过调用其QeueEnvelope方法和
    信封的克隆副本来订阅
    接收者

  • 通过以下方式将延迟的信封(和信封包裹的消息)发布到订阅的
    接收者:首先将它们包裹在内部轻量级线程
    (TMEDelayedEnvelopeDeliverer)中,该消息使消息管理器本身
    在延迟过去后将它们传递给

  • Sends messages to subscribed procedures (as RTL)
  • Sends messages to subscribed object methods (as RTL)
  • Sends envelopes to subscribed receivers by calling their ReceiveEnvelope method
  • Posts envelopes (and envelope wrapped messages) to subscribed receivers by calling their QeueEnvelope method with a cloned copy of the envelope
  • Posts delayed envelopes (and envelope wrapped messages) to subscribed receivers by enqueing them first in an internal lightweight thread (TMEDelayedEnvelopeDeliverer) which has the message manager itself deliver them when the delay has passed

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

As a receiver, the TMEMessageHandler implements the ReceiveEnvelope by simply delegating to the OnReceiveEnvelope event-handler.

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

Posted envelopes are received by the QueueEnvelope method, which adds the envelope in its thread-safe queue and then, but only if AutoProcessEnvelopes is True, uses the main thread's Queue to call its own ProcessEnvelope method (as by Remy's suggestion):

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方法(与由消息管理器处理同步消息),然后释放信封(请记住,这只是该接收者的克隆副本):

The ProcessEnvelope method extracts the envelope from the thread-safe queue, calls the ReceiveEnvelope method (same as called by the message manager for synchronous messages) and then Frees the envelope (remember that this was a cloned copy just for this receiver):

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方法只调用前者多次以清空异步消息队列:

The ProcessEnvelopes method just calls the former as many times as necessary to empty the asynchronous message queue:

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



如何使用TMEMessageHandler



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

How is the TMEMessageHandler used

Having defined TMELogMessage as an IMEClonableMessage to handle information to log, a minimal implementation for TMEFileLogger and other components looks like this:

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;

很长的帖子很抱歉,但是我希望这对其他人有用。

Sorry for the long post, but I hope this can be useful to someone else.

这篇关于如何从主UI线程中处理异步队列?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆